<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Knut Melvær</title>
    <link>https://knut.fyi</link>
    <description>developer edu and community @sanity.io ⋅ (he/him) ⋅ ask me about real-time text fields in front of delightful JSON ⋅ endorsed for dry jokes on linkedin ⋅ follow @knut.fyi on bluesky ⋅ norwegian who moved to oakland, california </description>
    <language>en</language>
    <lastBuildDate>Sun, 12 Apr 2026 08:38:39 GMT</lastBuildDate>
    <atom:link href="https://knut.fyi/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>A year in developer relations</title>
      <link>https://knut.fyi/blog/2026-03-16/a-year-in-developer-relations</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2026-03-16/a-year-in-developer-relations</guid>
      <pubDate>Mon, 16 Mar 2026 16:32:14 GMT</pubDate>
      <description>I’ve been running developer relations for Sanity.io for a year. Here is what I have learned so far. </description>
      <content:encoded><![CDATA[<p>Here&#x27;re my learnings based on my first year running developer relations at Sanity.io. I guess they are rather obvious, but I think they’re worth sharing as they can be helpful for someone starting with developer relations/advocacy. </p><p>I’ve been in developer relations for about a year now. Actually, I’ve only <em>known</em> that “develop relations” and “developer advocate” have been specific things for that long. The first time I consciously encountered these titles was when I was offered the role as &quot;Head of developer relations&quot; with my current job at <a href="https://www.sanity.io">Sanity.io</a>. It seemed to encapsulate with what I could contribute with.</p><p>Even though I had never worn such titles before, I had experience in teaching, mentoring, and dissemination, and I had been promoting aspiring developers before. I have a lot of training from teaching theory and methodology at University, I have run web development courses for retirees and beginners, and I have been writing about technology for a long time. I just didn’t know that that was a dedicated job in tech, and not the least, <a href="https://thenewstack.io/devrel-and-the-increasing-popularity-of-the-developer-advocate/">a “scene”</a>. With T-shirts, stickers, <a href="https://www.amazon.com/Business-Value-Developer-Relations-Communities/dp/1484237471">books</a>, and <a href="https://devrelcollective.fun/">Slack groups</a>. With the title, I was also joining a community and a discourse.</p><h2>1. Find yourself a team that cares</h2><p>I joined Sanity when they mostly were the developers from the original <a href="https://bengler.no">Bengler</a> team. They&nbsp; made the platform because they wanted a real-time content backend for <a href="https://www.oma.eu">OMA’s website</a>. In 2017, they converted Bengler into a startup for Sanity. Since I joined, I had to figure out how to do the developer relations part, but also contribute to how we should communicate Sanity in general. I’m very fortunate to be working with some fantastic people, both in terms of talent and experience, who also give an excellent mix of constructive feedback and praise.</p><p>That’s perhaps the most crucial part of what motivates me as a developer advocate: It&#x27;s much more fun when you and your colleagues feel that there’s a stake, that there‘re care and attention given to almost every detail of what we do. I get the sense that we have made this cool thing that we want people to use, because we think it makes their day better, and they will make cool things with it. If you want to work as a developer advocate, be on the lookout of a team that cares. It’s easy enough to forget if the product has a cool flair, or the perks are awesome.</p><h2>2. Learn to take feedback</h2><p>That stake, and care, comes with a challenging side too. There will be a lot of opinions, inclinations, ideas, and feedback on whatever you do. The feedback may even be conflicting or just not feasible to follow up. Your job is to mediate the feedback that you get within the company, with that you get outside, and turn it into something constructive and actionable. </p><p>I try my best to communicate why we are doing what we do in the weekly all-staff meetings. I also make sure to include nice things people have said about our work to remind the team that they’re doing work that&#x27;s appreciated by real people. That&#x27;s the “relations” part of the job.</p><p>I give much consideration to the feedback I get from my colleagues, but I don’t always follow it. That&#x27;s partly having to follow my gut-feelings from all the impressions and observations I&#x27;ve, being &quot;out there&quot;, and partly because the advice you&#x27;re given is for inaction (“We shouldn&#x27;t do x, y, z”).</p><p>I remember that we had a lot of discussion and indecisiveness around creating a new developer community. All the services had drawbacks for sure. My job then was just to pick something, and get it going. So I did. A year after we’re over 1.100 developers in <a href="https://slack.sanity.io">our Slack</a>. Yes, the message retention sucks, yes, it would have been handy to have all our answers indexed by Google. At the same time, the feedback, enthusiasm, and all the questions have been invaluable. We don’t regret it.</p><h2>3. Talk less, listen more</h2><p>Speaking of the community Slack. I also remember being super worried about trolls and people that would object to our <a href="https://github.com/sanity-io/sanity/blob/next/CODE_OF_CONDUCT.md">Code of Conduct</a>. It’s super interesting to see that many of us in tech are occupied with figuring out how we can make sure that people feel welcome, included, and safe. Since I became more aware of people’s shitty experiences, and my own mostly hassle-free experience in tech, I must admit that I many times have been afraid of inadvertently saying something that may cause someone discomfort.</p><p>Because I used(?) to be much more of a &quot;here’s my five cents”-guy on Twitter and such (well 600 words in, I guess I still kinda am), but I have consciously abstained from posting and replying to stuff that may have irritated me. I use two thought technologies (which I picked up from the <a href="http://5by5.tv/b2w/">Back to Work podcast</a>). The first is saying out loud: &quot;I will not let this bother me.&quot; The second is to question &quot;Is this the hill I want to die on?&quot;. They actually work — most of the time.</p><p>Instead of being such a smart ass, I have been trying to observe and listen instead. It is something I still work on being better at. I have been listening in two ways. First, I have made sure to follow a more diverse cast of people, and I have been trying to take in the stories of those who display concerns about the inclusiveness of tech. I’ve also tried to <a href="https://www.knutmelvaer.no/blog/2018/09/making-tech-survivable-what-can-men-do/">contribute with some thoughts</a> and took part in organizing <a href="https://www.globaldiversitycfpday.com/events/91">Global Diversity Call For Papers Day</a>. And it has been humbling. And frankly, it’s nice not being the one offering the hot take or glib tweet (well, there’s still some occasional ones).</p><h2>4. Learn how to take time off</h2><p>Honestly, I’m still rubbish at this. So I write this as much as advice for myself as for you. Thing is, there’s an infinite amount of tasks you can do as developer advocate: there’s always another talk proposal, demo, blog, or some documentation you could improve. There are always people that need help with something. So you have to draw some lines. Now, it’s hard to offer exactly what those lines should be because it also is highly dependent on where you live, whom you live with, if you have a family, how old you are, etc. </p><p>I’m fortunate in having gotten professional help for recurring depressions, and have mostly figured out how to live with it (<a href="https://en.wikipedia.org/wiki/Cognitive_behavioral_therapy">CBT</a> worked for me). I’ve been through a burn-out experience when I did my Ph.D. studies at University and got into tech after that – where I felt much more at home. I’m also fortunate in having a spouse that rides dressage. I often tag along to the stables to get some honest off-screen time with our two horses and help her out with grooming, and the many tasks and sorts that you have to do there. I also do my best to get some exercise, jogging and such. Doing something completely different seems essential.</p><p>Taking time off is partly on you, but having a workplace that understands that highly motivated people often need help with setting boundaries and be reminded that they need to take time off to not burn out at the office, is important as well. We’re regularly reminded of this at Sanity, and we’re trying to build structures (everything from how projects are planned, to setting expectations) that enable and ensure this as well. </p><p>I also think it’s vital for us that has the roles of being a developer advocate to communicate this as well, and to our own discretion share those part of the daily life. It’s easy enough to feed into people’s impostor syndromes and impression that you have to work all the time to belong in tech. </p><h2>5. Take it seriously, but don’t be too serious</h2><p>As a developer advocate, you do an important job. You’re both responsible that other people are successful at what they try to do, and on relaying back to your team what you learn. You are inevitably tied to your workplace and — excuse the marketing term — its brand. If you are approachable and helpful, that will contribute a lot to your company’s success. Even if I had put “opinions are my own” in my twitter bio, my behavior will reflect something back on people’s impression of Sanity, and the work being done there. So I owe to my team to be mindful of how I act and communicate. This position is very different from coming from the Humanities, where you are much more your own agent (at least, this was the case where I was studying). </p><p>This connection that you have with your workplace, and the fact, lets be honest, that much of what you’re doing can fall in under the term marketing, also means that people will have certain reasonable expectations of your biases. The serious part is not trying to be coy with whom and what you are representing, and be transparent when you’re discussing things where you obviously have an interest if promoting whatever you do. </p><p>At the same time, you are also a person with all the flaws and particularities that comes with that. Sometimes, people will say disparaging things about whatever you’re working on. Or they will dismiss you as a just a biased representative of where you work. That is the time where you shouldn’t be tempted to go into arguments or make a big fuzz. Try to distill whatever legit criticism can be found, and move on.</p><p>---</p><p>Being a developer advocate is probably the most exciting job I have ever had. The range of different things you can do and the people you encounter is ideal if you’re motivated by learning new things and are excited by learning what other people are creating. It has been an absolute blast being part of launching the developer community for Sanity, and we have a pretty interesting year ahead of us opening a new office in San Francisco, hiring more people, and preparing for the next step.</p><p>I look forward to looking back at the coming year with a new blog post like this. Stay tuned (e.g., by <a href="https://www.knutmelvaer.no/rss.xml">subscribing to the RSS feed</a>)! </p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/19bca66bb03d5857f71ecb653094c5e5bdfab4d3-4032x3024.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Config2020: Some impressions from Figma’s conference</title>
      <link>https://knut.fyi/blog/2025-12-20/config2020-some-impressions</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/config2020-some-impressions</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>I was at Figma’s first conference. This is what I took away from it.</description>
      <content:encoded><![CDATA[<p>During my month-long stay in San Francisco, mainly tinkering in the <a href="https://heavybit.com">Heavybit</a> offices, I had the opportunity to attend <a href="https://figma.com">Figma’s</a> first conference, cleverly named <a href="https://twitter.com/hashtag/config2020">Config</a>. Identifying more as a developer, than a designer, I wasn&#x27;t sure if the conference was “for me”. But I&#x27;m a huge fan of Figma. Both because we use it at <a href="https://www.sanity.io">Sanity.io</a> (where I work), and the product kinship that we have: real-time, collaborative, and exstensible applications, for people who make things.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/4adb5c32864e6495f7b4b9b870988b5dec783743-1920x1200.png?w=800&fit=max&auto=format" alt="The entrance for Config" /><figcaption>The venue entrance</figcaption></figure><p>Honestly, I wasn’t fully prepared to roam amongst 900+ handsome people towards the end of a pretty busy week, in a pretty busy start of the year. I instantly felt my introvertism blossom arriving at the venue on <a href="https://themidwaysf.com/">The Midway</a>. I got it somewhat tempered by caffeinating on the Diet Coke I fell back on because of endless coffee lines. Fortunately, I had a <a href="https://twitter.com/coreyward">conference buddy</a>, and the overall mood was super friendly.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/15bd179e1c19c9d1ddbda8605a3c6f24d0b1dfb4-1920x1970.png?w=800&fit=max&auto=format" alt="Config’s conference badge with my name and company on it" /><figcaption>Kudos for a badge where you can actually read the name. Didn't notice the stickeres with pronouns++ before I got back from the conference though (they inlayed in the back of the notebook you got on arrival)</figcaption></figure><p>Having been somewhat involved in <a href="https://vimeo.com/297890293">Webdagene</a> (Norway’s largest web conference that turned into <a href="https://www.y-oslo.com/">Y Oslo</a>), I know how hard it is to organize these things. There should have been more coffee stations, some re-thinking of the people logistics, and pre-signup with limited attendees to the workshop sessions. That being said, I’m super impressed with what the Figma team managed to pull together for their first conference. I have high expectations for next year! (because there will be one next year, right?)</p><p>Following are my take-aways from some of the talks I attended. As of now, only the live-stream from the first keynotes is out, but I’ll add the rest of the videos once they’re published.</p><h2>Welcome to a new decade of design, <a href="https://twitter.com/zoink">Dylan Field</a></h2><p><a href="https://youtu.be/x52k6RdJUyw">Watch on YouTube</a></p><p>My first real-life encounter with Dylan, the CEO/Founder of Figma, was actually when a couple of colleagues and me got to visit their HQs back in August 2019. We were already starry-eyed when we ringed the front-door. This super-friendly eerily familiar person let us in and found our host there. The shy Scandinavians that we were, and honestly, a bit nervous, we forgot to introduce ourselves properly. Not before after did I realized that we were let in by Dylan (<a href="https://twitter.com/zoink">I blame his artsy Twitter avatar</a>).</p><p>Dylan opened Config pretty much the same way. He set the mood with a down-to-earth approach and a lot of humor that made the floor filled with 900 ambitious and highly skilled people, that’s also aware of the other ambitious and highly skilled people, a bit more relaxed. Of course, he was there to launch some neat features, like better font-pickers (it’s fascinating how “small” improvements may save so many people a lot of time), and new auto-layout gizmos (huge cheer), and deep-linking to make it easier to get your collaborators to the right places.</p><p><a href="https://youtu.be/xL_ruBAwVmo?t=315">Link to Dylan‘s keynote with timestamp</a></p><p>Dylan also included a highly appriciated throwback to <a href="https://twitter.com/fat">@fat</a>’s legendary Pencil Tool video.</p><p><a href="https://twitter.com/fat/status/917841803578970112">View on Twitter</a></p><h2>Adding scaffolding to collaboration for deep work, <a href="https://twitter.com/craigmod">Craig Mod</a></h2><p><a href="https://youtu.be/WrAxvpvfB2w">Watch on YouTube</a></p><p>Dylan was followed by another friendly guy, that introduced a bit of vulnerability and self-reflection on the stage. My inner monologue watching Craig was a mix of “dammit, that’s clever, I wish I had thought of that” and “this is so relatable!”. I think everyone felt it when he talked about making a humongous book out of all the design comps and git commits from the Flipbook development. And a bit of envy from his long-ass walk from south of Tokyo to Kyoto.</p><p>Craig’s talk made me reflect more about how you should actively set healthy constraints for how you approach work, especially creative work. With the constant influx of notifications, it’s sometimes tempting to go full Luddite. What I liked about Craig’s approach, is that he considered how he could use technology and certain rules to both improve the mental environment, but also create other, and perhaps more interesting connections with people. Playing around with synchronicity and asynchronicity.</p><p><a href="https://youtu.be/xL_ruBAwVmo?t=2038">Link to Craig’s keynote with timestamp</a></p><h2>I pressed ⌘B. You wouldn’t believe what happened next, <a href="https://twitter.com/mwichary">Marcin Wichary</a></h2><p><a href="https://youtu.be/kVD-sjtFoEI">Watch on YouTube</a></p><p>There was a lot of buzz around this talk upfront. With good reason. This was probably the highlight of the conference. Partly because what Marcin brought to the stage is so very relatable for someone who works in a product company. Namely, the complex work that may behind the seemingly simple features. This is especially true when you work with a product that’s real-time and collaborative.&nbsp; </p><p>Marcin is brutally honest and equally funny. And the slides were brilliant too. I can&#x27;t really reveal much more of this talk, because it has some spoilers. I guess you just have to see it when it&#x27;s out.</p><h2>States; a product love language, <a href="https://twitter.com/volanStudio">Lucas Smith</a></h2><p><a href="https://youtu.be/9ohJWYx6dZw">Watch on YouTube</a></p><p>Lucas Smith’s musings about <em>state</em> in product design resonated with something I many times have felt as a frontend developer. Especially back in the days when I got handed stills from photoshop and such. I believe it’s also some of the motivation behind the “designers should code” thing. His distinctions between imperative and declarative modes of working with the design made a lot of sense too. There’s a negotiation between “how it looks” and “how it should work”. I believe it’s mighty helpful to both aware and to communicate this when you present design and ask for feedback.</p><h2>Open source design. Design is the future of open source., <a href="https://twitter.com/soleio">Soleio</a></h2><p><a href="https://youtu.be/2jlt-1E1Ee0">Watch on YouTube</a></p><p>I didn’t expect to feel as much at “home” at this conference as I did. Because I’m more of a developer than a (visually oriented) designer (cue the “everyone is a designer” debate). But I really connected with most of the talks because there so many concerns that are in common. Open source is one of them, and it was interesting to hear Soleio talk about his views on open source. Not just inherently interesting, but also because Soleio is founder and partner at the VC fund Combine. As someone working at a startup, I seek to understand the <em>weltanschauung </em>of those investing in technology and design.</p><p>Soleio suggested that open source is perhaps less about the access to the material (i.e., the code, or the vectors), and more about that it allows people to reshape what it can be used for. His prime example was the invention of spreadsheets for budgeting and calculus, and how it was reshaped by the <a href="https://www.vox.com/culture/2018/1/11/16877966/shitty-media-men-list-explained">Shitty Media Men List</a> and the #metoo movement.</p><p>I also appreciated the rush of nostalgia when Soleio brought forth <a href="https://joshuadavis.com/ps1-praystation-v2">Joshua Davis’ PrayStation CD-ROM</a>, released back in the days when Flash was a thing.</p><h2>Open source design. Designing with the Community., <a href="https://twitter.com/miguelsolorio_">Miguel Solorio</a></h2><p><a href="https://youtu.be/yZUZk9B-PlE">Watch on YouTube</a></p><p>Miguel is the only designer in the surprisingly small team behind VS Code. He told us how his team involved the community in their design processes. And by the community, we’re talking one of, if not the, most popular code editor. That seems like a stupendous amount of pressure when you have to make design decisions that affect a tool that millions of people use for their work every day. Again, talks are always more interesting when they talk about hard-earned lessons, and they came improved from it. Definitively one of the talks I’ll revisit for inspiration on how we can get better at involving Sanity’s community when we’re figuring out new stuff.</p><h2>Joyful Subversions, <a href="https://twitter.com/mayli">May-Li Khoe</a></h2><p><a href="https://youtu.be/Aalyplbv5Mo">Watch on YouTube</a></p><p>It’s not often I get teary-eyes at conferences. But May-Li <em>subverted</em> my professional conference attendance posture and dug right down to both existentialism and humanity. It’s so easy to get bogged down in all the bubbly concerns in growing a startup, that it’s easy to forget that what you are making and spending time on realizing, may be used to improve people’s lives in small and big ways.</p><p>I was amused to learn that the turntable icon that I chose for my mac’s user account because I didn’t have any selfies at the time, and never bothered to change it, was made by May-Li.</p><p>I think I’ll just keep indefinitely.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/d64af8cfea6bc73f2509f7e3248568486dea5f0c-1336x972.png?w=800&fit=max&auto=format" alt="The system preferences panel with the vinyl player avatar" /><figcaption>At least my Mac thinks that I'm a DJ. (well, my Spotify Discovery Weekly tends to be pretty awesome, so I do rock in algorithms)</figcaption></figure><p></p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/4adb5c32864e6495f7b4b9b870988b5dec783743-1920x1200.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>This post will be free, and I will not track you.</title>
      <link>https://knut.fyi/blog/2025-12-20/this-post-will-be-free-and-i-will-not-track-you</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/this-post-will-be-free-and-i-will-not-track-you</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>I&apos;m back with my own blog on my own domain after some years on other people’s platform. And it feels good.</description>
      <content:encoded><![CDATA[<p>I published my first post on Medium back in 2016. I fell for the same focused, uncluttered interface that drew many other writes to the website. I was impressed that there didn’t seem to be any spam or rubbish comments (I might just be tricked by the spam looking good). It was a good trouble-free space to be in.</p><p>Then it changed. Medium needed to find a way to make our writing and your reading their business. I guess that’s better than making our attention and behaviour their business, although I suspect that this is the case too. So Medium has put up nag screens reminding us that we need to pay them. Fair enough. But I don’t want my readers to have to pay for my blogging, or click through pop-ups. So I’m moving back to my own little corner of the web.</p><p>I put up my first self-made website on the internet in year 2000. Back then I blogged in hand-written HTML, before I got into PHP and fiddled around with early CMSs before adopting Wordpress. I’ve been on LiveJournal and blogger too. When I pushed code for money, mainly building Wordpress sites,&nbsp; I started using Squarespace for my own blog, partly because I wanted to try it out, and partly because I wanted my space to be hassle-free (I was sysadmining enough Wordpress sites as it where).</p><p>This post is published on my own blog on <a href="https://www.knutmelvaer.no">knutmelvaer.no</a>, and then, as the last gesture, imported into Medium giving it the proper canonical URL while that still is possible. From here on, if you want to follow my writing, you have to visit <a href="https://www.knutmelvaer.no/">the webpage</a>, subscribe to <a href="https://www.knutmelvaer.no/rss.xml">the RSS</a> when I have gotten that fully up and running, follow me on <a href="https://dev.to/kmelve">dev.to</a> (where I’ll syndicate my posts), or just pay half-attention to my <a href="https://twitter.com/kmelve">twitter feed</a>.</p><p>There hasn’t been much writing on my own part the last year either. Mostly because I’ve been occupied by figuring out how to be a developer advocate in my new job at <a href="https://sanity.io/">sanity.io</a>. But I figured it’s time again. My new blog is made from the JAMstack <a href="https://www.sanity.io/create?template=sanity-io%2Fsanity-template-gatsby-blog">Sanity.io + Gatsby + Netlify starter</a>. I’ve started tweaking it to make it my own, it feels very much as the exciting early years of my web development adventures.</p><p>I have also started implementing <a href="https://indieweb.org/microformats2">microformats2</a> and <a href="http://webmention.io/">webmentions.io</a>, which I’m excited to learn more about. Expect that other nifty features will enter this little web realm of mine too.</p><p>See you in cyberspace!</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/80dde21eec72aeca7c8d59d8bd546a2af04713fd-2396x1724.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>10x engineers and my approach to Twitter</title>
      <link>https://knut.fyi/blog/2025-12-20/10x-engineers-and-my-approach-to-twitter</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/10x-engineers-and-my-approach-to-twitter</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>How a thread about 10x engineers made me reconsider how I approach Twitter. (This post isn&apos;t really the 10x engineers thing).</description>
      <content:encoded><![CDATA[<p>You&#x27;re probably a bit tired of the whole 10x engineer thing by now, but this post isn&#x27;t really about that. It is about what I learned from it and how I’m reconsidering my approach to Twitter.</p><h2>10x what? Context, please.</h2><p><a href="https://twitter.com/skirani/status/1149302828420067328">View on Twitter</a></p><p>Investor Shekar Kirani recently posted a thread on twitter about the traits to an alleged type of engineers/programmers. I think it&#x27;s safe to say that they are a combination of stereotypes that many people find toxic and problematic in a work environment. The ethos of the tweet seems to be that there are these people that don&#x27;t fit in with the normal expectations of the workplace (“they don’t like meetings” etc), but will be these efficient, high performing workers that will give you, the founder, an advantage in the race that is product development.</p><p>I&#x27;ll leave it to others to rebuke each and every argument in this thread (just dive into the replies to get an idea), but let me just point out that the best coders I know have tended to be really nice people that have happily shared their knowledge, gone to meetings, had family obligations, and all sorts of desktop backgrounds.</p><h2>I piled some wood on the bonfire</h2><p>So Shekar’s tweets struck a nerve and the reactions were plentiful. People were tired by the whole 10x thing before two hours had gone. The sentiment among the outspoken programmers and tech people were pretty unified against the idea that the profile the tweets painted had any basis in reality, and were a collection of traits that had no business being in tech or a workplace. There were also worry that these tweets rehearsed and perpetrated ideas that are shared among founders and investors. Some of the generalizations also made it really easy to mock and satirize the twitter thread.</p><p>As I&#x27;ve written before, I&#x27;m sometimes tempted to do the hot take or go for the satirical smartass comment on Twitter. Shekar’s thread got to me, both because here was this investor promoting something I deeply disagree with, but also because it seemed so out of place to do so in an environment that&#x27;s just ready to pile on people that seemingly work against making tech a healthier and more inclusive place.</p><p>So I tweeted something to the effect that I think it&#x27;s weird that an investor would say these things publicly, and should have known the response it would get. It simply doesn&#x27;t seem like a strategic move, even though if one means it. Only I choose to put emphasis on what these tweets said about Shekar as a person, and by “quote tweeting” him. Making the recipients my audience, and engaging him directly. </p><p>I got my likes and replies and moved on with the day. I was one of many hundreds, if not more, that more or less mocking wrote Shekar and his thread off. As one does on Twitter, right?</p><h2>Is this the person I want to be?</h2><p>And then I got some feedback. A person, whose opinions I care about, told me that I came off arrogant and with little respect to a person I disagreed with, who I didn’t know, on the basis on some tweets. My first reaction was to become a bit annoyed. I just did what you do on Twitter, right? Here&#x27;s this investor saying outrageous stuff, and I was sticking it to the man. But then I had to reconsider the critique because it came from a place of concern.</p><p>I don&#x27;t know Shekar. I don&#x27;t know much about the context he&#x27;s working on in India. But I know that the respect and humanity I found lacking in the ideas behind his tweets were a bit lacking in mine as well. I didn&#x27;t tweet what I did because I hoped to change anyone&#x27;s mind or educate them.</p><p>I tweeted what I did because I wanted to look smart and part of “good team“. I was virtue signaling. And doing so by throwing timber on a bonfire to a person that probably didn&#x27;t expect how this thread would blow up. That&#x27;s not a person I want to be, and I don&#x27;t think I really was helping promote the ideas and practices we so dearly want in tech.</p><p>So I deleted my tweets.</p><h2>I’m not done thinking about how I approach Twitter</h2><p>My reconsideration of how I approach Twitter can easily be read as a value judgment of others’ satirical or mocking replies to Shekar’s tweets. This is where I’m ambivalent. Because I do mean and appreciate that there is a place of satire, humor, and jestful creativity to counter destructive ideas put forth by people in power. The site <a href="https://1x.engineer/">1x Engineer</a> is a good example of this.</p><p>There’s also another side of this, which is that of privilege. I have, as a Norwegian guy in my early 30s, very little reason to feel particularly angry or hurt based on my experiences growing up in one of the safest environments on this planet. But other people have plenty of good reasons to feel and express both.</p><p>That’s why I explicitly am framing these thoughts as the reevaluation I’ve done for my own behavior, and put them here in a way to think loudly in public, and to hold my self accountable. So this is how I&#x27;ll try to approach critique on the web going forward. And frankly, most of these points are not original and have been made by many others before me:<br/></p><ul><li>I&#x27;ll consider that I’m responding to a real person.</li><li>I’ll comment on the actions and ideas represented in the tweets.</li><li>I’ll consider that the person may have English as a secondary language (as I do), and are not aware of nuances and the implications that certain word choices may have.</li><li>I’ll consider what I want to achieve: Is it providing nuance or reconsideration? Is it just blowing off steam? Is it marking a stance?</li><li>I will to the best of my ability, try to make it possible for the person to reconsider, or engage me in a discussion.</li></ul><p>I believe I know what you’re thinking: What about trolls and people with extreme and hurtful opinions? I don’t really think all people on Twitter is there for “enlightened discourse”, which the points above are aiming for. And obviously, many people know what they’re doing when they say hurtful stuff. As for now, I’ll probably not grant them much of my attention and counter with promoting ideas and activities that make tech a safer and more inclusive (for example, <a href="https://www.knutmelvaer.no/blog/2018/09/making-tech-survivable-what-can-men-do/">Making tech surviveable: What can men do?</a>).</p><p>---</p><p>Is there something I have failed to consider here? Did this make you think about how you use Twitter? I’d love to hear about it!</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/3df2da032c026083e07a5b5a1fc9e48fba88148b-1920x1280.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>The Web Project Book’s “Implementing Backend”: Putting Sanity.io to the test</title>
      <link>https://knut.fyi/blog/2025-12-20/a-practical-application-of-the-web-project-book</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/a-practical-application-of-the-web-project-book</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>How I would approach the “implementing backend” chapter of &quot;The Web Project Guide” with Sanity.io. </description>
      <content:encoded><![CDATA[<p>I have been following the work of <a href="http://deanebarker.net/">Deane Barker</a> (aka <a href="https://gadgetopia.com/">Gadgetopia</a>) for a while. And although I often find myself disagreeing with him, it&#x27;s hard to get away from that much of the work on CMS he has been involved is pretty darn insightful. One of his projects is “<a href="https://www.webprojectbook.com/">The Web Project Guide</a>” that he writes together with <a href="https://twitter.com/mrvilhauer">Corey Vilhauer</a>. I recently got aware of <a href="https://www.webprojectbook.com/implement-backend/">the chapter “implementing the backend functionality”</a>, which in this case mostly deals with the content management system.</p><p>So, early disclaimer: I work for <a href="https://www.sanity.io">Sanity.io</a>, where we&#x27;re making what we call the platform for structured content. It replaces your CMS, but it also gives you more power and flexibility when it comes to how to interact with that content, not just in terms of distribution. It lets you treat content as data. That&#x27;s the key.</p><p>So, of course, read this as content marketing if you want, but I hope you can get something out of it if you&#x27;re interested in content management. And yeah, if Deane can be strategic director over at <a href="https://en.wikipedia.org/wiki/Episerver">Episerver</a> while writing this guide (he started it before taking on this position, I believe), I think it&#x27;s OK for me to see their writing through the lens of Sanity.</p><p>This post makes most sense if you have at least scrolled through <a href="https://www.webprojectbook.com/implement-backend/">their chapter</a> beforehand, but I&#x27;ll try my best to give enough context to make sense. This also why I follow the same structure in terms of headings. With that out of the way, let&#x27;s jump into it!</p><blockquote>Most CMSs can publish content in some form out-of-the-box, but they have to be...persuaded to do it in a way that fulfills your requirements.</blockquote><p>I went ahead and initiated a new project with the “clean templates”, which comes with no content types at all. So little persuasion needed, in fact, Sanity.io is designed not to have to be persuaded into being what you want, but exactly the opposite, it&#x27;s built to be configured and customized to precisely what you need (<a href="https://sanity.io/create">you can give yourself a head start though</a>).</p><h2>Model implementation</h2><blockquote>The model implementation is your content model working inside your chosen CMS.</blockquote><p>With Sanity.io you get an open-source application that functions as the CMS in most cases. You do model implementation by <a href="https://www.sanity.io/docs/content-modelling">describing schemas in simple JavaScript objects</a>. This is somewhat different compared to most CMSs where you do it with forms in a graphical interface. It may seem more difficult, but it lets developers and people with some JavaScript skills to version control schemas, and have a short way to do more advanced stuff like custom field validation and code-based generation of schemas. It also makes the distance to customizing input components much shorter.</p><p>The <a href="https://webprojectbook.com/implement-backend/#model-implementation">guide mentions</a> that a model is a combination Types, Attributes, and Validation Rules, and uses an “Article” (type) with “Title” and &quot;Publication Date&quot; (attributes), and “minimum length” (validation) as an example. If you run <code>npm i -g @sanity/cli &amp;&amp; sanity init</code> and go through the steps, you&#x27;ll be ready to make the content model within a couple of minutes. Here&#x27;s how you make the minimal article example with Sanity:</p><pre><code class="language-javascript">// article.js

export default {
  name: 'article',
  type: 'document',
  title: 'Article',
  fields: [
    {
      name: 'title',
      type: 'string',
      title: 'Title',
      validation: Rule =&gt; Rule.min(10).warning('The title should be longer')
    },
    {
      name: 'publishedAt',
      type: 'datetime',
      title: 'Publish date',
      description: 'Choose a date for when the article should be published'
    }
  ]
}</code></pre><p>When you run the studio (it&#x27;s a single page application built with React) this code will generate a user interface that looks like this:</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/0bfdf661b0e0f6ac8584a9b968c02037986be586-2400x1800.png?w=800&fit=max&auto=format" alt="Sanity Studio with a minimal article example" /><figcaption>The interface for the article schema above </figcaption></figure><p>So far, so good. Where it gets interesting is when it comes to content model limitations. Sanity.io is built for structured content, so we have gone far to make it as flexible as possible.</p><h3>Limitations?</h3><blockquote>However, sometimes you just can’t wrap a CMS around your model requirements.</blockquote><p>Here the authors come with two examples:</p><ul><li>The model supports linking the <em>Article</em> object to an <em>Author</em> object, but it doesn&#x27;t let you go in the other direction, i.e. the link isn&#x27;t bi-directional.</li><li>Your model requires a <em>Meeting</em> object to have sub-objects for <em>Topics</em>. Each <em>Topic</em> is also a content object.</li></ul><h3>Bi-directionality</h3><p>Let&#x27;s add an author type and a new field to the article which is an array of references to the author type (because you want multi-author support, don&#x27;t you?):</p><pre><code class="language-javascript">// author.js

export default {
  name: 'author',
  type: 'document',
  title: 'Author',
  fields: [
    {
      name: 'name',
      type: 'string',
      title: 'Author',
    },
  ],
}</code></pre><pre><code class="language-javascript">export default {
  name: 'article',
  type: 'document',
  title: 'Article',
  fields: [
    // the other fields,
    {
      name: 'authors',
      type: 'array',
      title: 'Authors',
      of: [
        {
          type: 'reference',
          to: [{ type: 'author' }],
        },
      ],
    },
  ],
}</code></pre><p>When using the <code>reference</code> attribute/field, Sanity will index those references bi-directionally. So if you wanted to query all <code>authors</code> with their <code>articles</code> although the field is set in the article object, it can be done like this with GROQ using <a href="https://www.sanity.io/docs/groq-joins">joins</a> (<a href="https://4bb85k56.api.sanity.io/v1/data/query/production?query=*%5B_type%20%3D%3D%20%22author%22%5D%7B%0A%20%20name%2C%0A%20%20%22posts%22%3A%20*%5B_type%20%3D%3D%20%22article%22%20%26%26%20references(%5E._id)%5D%7B%0A%20%20%20%20title%0A%20%20%7D%0A%7D&%24slug=%22a-test-post%22">actual example</a>):</p><pre><code class="language-javascript">*[_type == "author"]{
  name,
  "posts": *[_type == "article" &amp;&amp; references(^._id)]{
    title
  }
}</code></pre><p>This query can also be baked into the Sanity Studio using <a href="https://www.sanity.io/docs/structure-builder-typical-use-cases#tabs-with-content-previews-a8e5cc70dbc0">split panes</a> to show “incoming references”. This is a minimal example (and of course, you can use the same logic to show “related” documents by pretty much any logic):</p><pre><code class="language-jsx">// deskStructure.js
import React, { Fragment } from 'react';
import S from '@sanity/desk-tool/structure-builder';
import QueryContainer from 'part:@sanity/base/query-container';
import Spinner from 'part:@sanity/components/loading/spinner';
import Preview from 'part:@sanity/base/preview';
import schema from 'part:@sanity/base/schema';

const Incoming = ({ document }) =&gt; (
  &lt;QueryContainer
    query="*[references($id)]"
    params={{ id: document.displayed._id }}
  &gt;
    {({ result, loading }) =&gt;
      loading ? (
        &lt;Spinner center message="Loading items…" /&gt;
      ) : (
        result &amp;&amp; (
          &lt;div&gt;
            {result.documents.map(document =&gt; (
              &lt;Fragment key={document._id}&gt;
                &lt;Preview value={document} type={schema.get(document._type)} /&gt;
              &lt;/Fragment&gt;
            ))}
          &lt;/div&gt;
        )
      )
    }
  &lt;/QueryContainer&gt;
);

export const getDefaultDocumentNode = () =&gt;
  S.document().views([
    S.view.form(),
    S.view.component(Incoming).title('Incoming references'),
  ]);

export default S.defaults();
</code></pre><p>This will produce this interface, and by the way, it&#x27;s real-time, so if someone adds this author to another article, it will appear in the right list without requiring reloading.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/673376ecfe5b453bcc3a0f092669dee3c40e309c-2400x1800.png?w=800&fit=max&auto=format" alt="Studio with split panes and incoming references" /><figcaption>Minimal example of incoming references.</figcaption></figure><h3>Nested structures</h3><p>The second limitation has to do with nested structures (parent-child). Personally, I tend to avoid putting too much hierarchy into content models, and I suspect it often comes from “sitemap” and “nested menu” thinking where you structure content to make navigation. I prefer representing navigation as a separate content structure with references because that lets us more quickly iterate on navigation structures, but also have different content trees for different presentation layers and purposes. But I digress, let&#x27;s look at the example:</p><blockquote>Your model requires a Meeting object to have <strong>sub-objects for Topics. Each Topic is also a content object</strong>. To do this, you need to connect a Topic to a Meeting in a parent-child model. However, your CMS doesn’t have a content tree that would allow this, nor does it allow nested objects. <strong>You can link a Topic to a Meeting, but someone else could link another Meeting to the same Topic (not allowed by the model), and it doesn’t stop the Meeting from being deleted and “orphaning” a bunch of Topics (also verboten).</strong></blockquote><p>There are mainly two approaches to this with Sanity, you can either just embed the topic object model in the meeting model because the schema allows for nested objects. You would still be able to query for only topics, and you will never have this orphanage or multiple meetings tied to the same topic, which in this case is unwanted. Here is a simple example:</p><pre><code class="language-javascript">// topic.js
export default {
  name: 'topic',
  type: 'object',
  title: 'Meeting topic',
  fields: [
    {
      name: 'name',
      type: 'string',
      title: 'Topic name',
    },
    {
      name: 'description',
      type: 'text',
      title: 'Description',
    },
  ],
}</code></pre><pre><code class="language-javascript">// meeting.js
export default {
  name: 'meeting',
  type: 'document',
  title: 'Meeting',
  fields: [
    {
      name: 'date',
      type: 'date',
      title: 'Meeting date',
    },
    {
      name: 'topic',
      type: 'topic',
      title: 'Meeting topic',
    },
  ]
}</code></pre><p>Notice how I have described <code>topic</code> as an object type here, and how I refer to this type in the <code>meeting</code> schema. The interface for this will look like this:</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/aa62459165fbb8946d5b057f6fb91e91b6b11c55-2400x1800.png?w=800&fit=max&auto=format" alt="Meeting document type with topic object" /></figure><p>And the data structure will end up like this:</p><pre><code class="language-json">{
  "_createdAt": "2020-04-09T11:21:22Z",
  "_id": "d9996973-58aa-44be-8a8a-c24c7df61b6e",
  "_rev": "lwxf4peCy8NTOg4fhvBuIK",
  "_type": "meeting",
  "_updatedAt": "2020-04-09T11:21:22Z",
  "date": "2020-04-09",
  "topic": {
    "_type": "topic",
    "description": "This is a nested object.",
    "name": "A unique topic tied to this meeting"
  }
}</code></pre><p>And if I wanted to query all meetings and return just the topics in my dataset, I could do it with this GROQ query: <code>*[_type == &quot;meeting&quot;].topic</code>.</p><p>Now, let&#x27;s make this a bit more interesting, and say that we wanted to implement topic as a dedicated document type, and use references to tie them to meetings. If we want to avoid orphan topics, we need to put the reference on the topic side. If a topic has a reference to a meeting, you can&#x27;t delete that meeting without removing either the topic or the reference first. Granted, the UI for this flow can (and will get) a bit smoother. But here&#x27;s how to do it:</p><pre><code class="language-javascript">// topic.js
export default {
  name: 'topic',
  type: 'document',
  title: 'Meeting topic',
  fields: [
    {
      name: 'name',
      type: 'string',
      title: 'Topic name',
    },
    {
      name: 'description',
      type: 'text',
      title: 'Description',
    },
    {
      name: 'meeting',
      type: 'reference',
      title: 'Meeting',
      to: [{ type: 'meeting' }],
    },
  ],
}</code></pre><p>This will produce this interface:</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/26ca8c02a81d255755040591d406ce0a9a397a4c-2400x1800.png?w=800&fit=max&auto=format" alt="A meeting topic with a reference to a meeting" /><figcaption>A meeting topic with a reference to a meeting</figcaption></figure><p>And if you now try to delete the referenced meeting, you&#x27;ll get this warning:</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/1848820ea7f54674658d17122c5395b8ab0c08ae-2400x1800.png?w=800&fit=max&auto=format" alt="Incoming reference warning" /><figcaption>If you try to delete a document with an incoming reference you'll get this warning</figcaption></figure><p>Sidenote: You can define references as <code>_weak: true</code> if you explicitly don&#x27;t want this integrity check. </p><h3>Editorial Experience</h3><p>The authors discuss the concepts of <em>Data Coercion</em> and <em>Data Validation</em>, in other words, making sure that it&#x27;s easy to let editors put in content in the correct format, and validate the content if it&#x27;s possible to enter it incorrectly. The first example is adding a publish date field for an article, where it doesn&#x27;t have a time stamp, and the date shouldn&#x27;t be in the future. Here&#x27;s how to do exactly that with Sanity:</p><pre><code class="language-javascript">{
  name: 'publishedAt',
  type: 'date',
  title: 'Publish date',
  description: "Choose a publish date. Can't be in the future",
  validation: Rule =&gt;
    Rule.custom(
      date =&gt;
        date =&lt; new Date().toISOString().split('T')[0] ||
        `This shouldn't be in the future`
    ),
}</code></pre><p>Sidenote: The <code>validation</code> property also supports promises and any logic you can express in JavaScript, which means that you can validate fields via APIs and whatnot. All fields come with <a href="https://www.sanity.io/docs/validation">common validation rules out-of-the-box.</a></p><p>The next example is rich text editing, but with constraints on formatting. Sanity’s rich text editor is configurable and extendable. It also saves the text into a structured and syntax agnostic format called <a href="https://www.sanity.io/docs/block-content">Portable Text</a>, so that even if you had formatting that you didn&#x27;t want in your presentation layer, it is pretty easy to ignore it in the implementation. A rich text field with only <strong>bold</strong> (or <code>strong</code>) <em>italics</em> (or <code>emphasis</code>) and linking looks like this: </p><pre><code class="language-javascript">// simpleRichText.js
export default {
  name: 'simpleRichText',
  type: 'array',
  title: 'Body',
  of: [
    {
      type: 'block',
      styles: [],
      lists: [],
      marks: {
        decorators: [
          { title: 'Strong', value: 'strong' },
          { title: 'Emphasis', value: 'em' }
        ]
      }
    }
  ]
}</code></pre><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/66783803d9dbc5e403a8e80b2c46f2d345c0e3f1-717x366.png?w=800&fit=max&auto=format" alt="A minimal rich text editor" /><figcaption>A minimal rich text editor</figcaption></figure><p>Moving on, adding a description to the title field explaining it&#x27;s used as the fall back SEO title, is also pretty effortless. And of course, you can have fields called <code>title</code> with different descriptions throughout the CMS:</p><pre><code class="language-javascript">{
  name: 'title',
  type: 'string',
  title: 'Title',
  description: `Used as the fallback if the SEO title isn't set`,
  validation: Rule =&gt;
    Rule.min(10).warning('The title should be longer'),
}</code></pre><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/456c4d608023e17c20650c3bbe84c464454e74b9-699x126.png?w=800&fit=max&auto=format" alt="The title field with a description that explains how it's used" /></figure><p>The last example is affordances for grouping fields into tabs or sets to lessen the cognitive load. Sanity comes with fieldsets out of the box, and somebody has made <a href="https://www.sanity.io/plugins/sanity-plugin-tabs">a plugin that expresses these as tabs</a>. </p><pre><code class="language-javascript">// article.js
export default {
  name: 'article',
  type: 'document',
  title: 'Article',
  fieldsets: [
    {
      name: 'meta',
      title: 'Metadata',
      options: { collapsed: true, collapsible: true },
    },
  ],
  fields: [
    // the fields
  ]
}</code></pre><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/f29511f1b88b9284191c0bdd76c5cd24c3c7d530-860x690.gif?w=800&fit=max&auto=format" alt="Collapsible fieldsest" /><figcaption>Collapsible fieldsets</figcaption></figure><p>All in all, it seems like Sanity meets all the examples given for content modelling and the examples mentioned. And we&#x27;ve just scraped the top of the ice-berg of what&#x27;s possible in terms of catering to an awesome editoral experience. Because, it&#x27;s true as the authors write:</p><blockquote>Cater to your editors. Implement your content model at a level that allows the CMS to help them. The happier they are, the better your content will be, and the longer your CMS implementation will last.</blockquote><p>The Sanity Studio comes with a lot of <a href="https://www.sanity.io/docs/schema-types">field types</a> out of the box, and lets you make <a href="https://www.sanity.io/guides/how-to-make-a-custom-input-component">your own custom input components</a> if you have special data coercion needs, and as we touched on, you can do field validation with promises and JavaScript. </p><h2>Aggregations</h2><p>Since the underlying data for Sanity.io are JSON documents, that can be queried and join on pretty much any key/value the need to explictly author aggregation descreases (want an implementation to group any document that starts with &quot;F&quot; in it&#x27;s title field (if it has one), no problem: <code>*[defined(title) &amp;&amp; title match &quot;F*&quot;]</code>. </p><p>But there are perfectly sound reasons to make explicit aggregations of content, so let&#x27;s go through the <a href="https://webprojectbook.com/implement-backend/#aggregations">different types</a> mentioned by the authors.</p><h3>A Content Tree</h3><blockquote>A very common pattern is a “tree” of content, where you have a &quot;root&quot; object with a hierarchy of descendants below it. Every object is a child of another object, and might have one or more children of its own.</blockquote><p>As I previously mentioned, I&#x27;m not a big a fan of structure content deeply into a hierarchy. Soon enough editors are required to be “hiearchy janitors”, and there almost always comes a point where you want to break out of that hiearchy or do it differently. But sometimes you actually need to put content into parent-child-like structures where they also have order. For those times, I tend to make another document type with fields to express hierarchies. </p><p>A practical example is how the documentation and its table of contents is done on <a href="https://sanity.io/docs">sanity.io/docs</a>. The articles is a flat list of the <code>article</code> type, while the menu is made as a document type called <code>toc</code> (as in Table of Contents):</p><pre><code class="language-javascript">// toc.js
const tocSection = {
  name: 'toc.section',
  type: 'object',
  title: 'Section',
  fields: [
    {
      type: 'reference',
      name: 'target',
      title: 'Target article',
      to: [{ type: 'article' }],
    },
    {
      type: 'string',
      name: 'title',
      title: 'Title',
    },
    {
      type: 'array',
      name: 'links',
      title: 'Links',
      of: [{ type: 'toc.link' }],
    },
  ],
};

const tocLink = {
  name: 'toc.link',
  type: 'object',
  title: 'Link',
  preview: {
    select: {
      title: 'title',
      targetTitle: 'target.title',
    },
    prepare: ({ title, targetTitle }) =&gt; ({
      title: title || targetTitle,
    }),
  },
  fields: [
    {
      type: 'reference',
      name: 'target',
      title: 'Target article',
      to: [{ type: 'article' }],
      description: 'No target article turns the item into a subheading.',
    },
    {
      type: 'string',
      name: 'title',
      title: 'Title',
      description: 'Override title from the target article.',
    },
    {
      type: 'array',
      name: 'children',
      title: 'Children',
      of: [{ type: 'toc.link' }],
    },
  ],
};


const toc = {
  name: 'toc',
  type: 'document',
  title: 'Table of Contents',
  fields: [
    {
      type: 'string',
      name: 'name',
      title: 'Name',
    },
    {
      type: 'string',
      name: 'title',
      title: 'Title',
    },
    {
      type: 'array',
      name: 'sections',
      title: 'Sections',
      of: [{ type: 'toc.section' }],
    },
  ],
};

export default { tocSection, tocLink, toc };</code></pre><p>This is a bit of code, but notice that this is also recursive, meaning that you can make sub-sections. We can also use <a href="https://www.sanity.io/docs/overview-structure-builder">Structure builder</a> to query this structure and group the documents accordingly in other views, which also allows you to browse the same documents by different criteria depending on what you&#x27;re doing.</p><p>This makes it very easy for us to both test and change the navigation structure if we want to. We could even A/B-test it, if we thought that was a good idea (probably not). Another nice byproduct is that since these are references, we can&#x27;t outright delete an article that&#x27;s put into a table of contents. So we keep content integrity even though this system doesn&#x27;t “know” about its presentation. </p><h3>Folders</h3><p>Sanity doesn&#x27;t have folders in the traditional sense, so folder like organization happens like the approach above. That being said, someone could totally make a folder tool to give people that affordance. But I&#x27;m not sure it&#x27;s worth the time?</p><h3>Menus or Collections</h3><p>This is pretty much covered by the example above. </p><h3>Tags or Categories</h3><p>You can implement categories using the reference field, and you can get ad hoc tags via the string field:</p><pre><code class="language-javascript">// tags.js
export default {
  name: 'tags',
  type: 'array',
  title: 'Tags',
  of: [{type: 'string'}],
  options: {
    layout: 'tags',
  }
}</code></pre><h3>Aggregation as content model or as editor experience?</h3><p>What I think it&#x27;s good with the way we approach content modelling at Sanity.io is that aggregation can be done either as an implementation detail through queries, as a content model concern through references, or as a workflow mode through structure builder. And these can be done independently.</p><h2>Content Rough-In</h2><p>It sometimes takes a bit of actual building, and some back-n-forth to get the content model right. Preferably, you have done some prior work before diving into the technical implementation. That being said, it&#x27;s so easy to iterate and test things out with the studio, that I often find myself just coding up fields while I discuss with my team how things should work. In that group there will be the ones who are actually making the presentation layer, as well as the people who will work with the content.</p><p>So although the authors explicitly state that the “rough-in” is <em>not </em>about migration, in my experience, if you go by a content-first approach, you&#x27;ll almost always need to move some stuff around early on. </p><p>We always try to get real content into the system as early as possible. That&#x27;s where the learning and the uncovering of the unknowns are. And almost always, you&#x27;ll discover that you need to structure something differently or rename a field. That&#x27;s where <a href="https://www.sanity.io/docs/migrating-data">migration scripts</a> or simply <a href="https://www.sanity.io/docs/export">exporting your whole dataset</a> and use find/replace-all and importing it again moves you along. Since your content comes as JSON documents, it&#x27;s a cakewalk to migrate compared to SQL tables with join tables and whatnot.</p><h2>Templating and Output</h2><p>The authors does a decent job of exemplifying how you integrate your content using a template language. In traditional CMSs you&#x27;ll get a built-in HTML rendering system, with a chosen templating language. With Sanity, the content will be available through APIs, which let you integrate with any front-end framework (and other things that&#x27;s not even of the web). The approach is fairly similar though you&#x27;ll have accessible “placeholders” (aka variables) that you can map out, iterate over, build logic from, and so on. The starter projects over at <a href="https://www.sanity.io/create">sanity.io/create</a> will give you a sense of how it can be put together.</p><h3>Templating languages</h3><p>It&#x27;s interesting that in the author‘s list of templating languages, there is a glaring lacuna: JavaScript (I guess there&#x27;s a certain headless CMS blindspot here?) But common templating languages, or front-end frameworks in the JavaScript world are:</p><ul><li><a href="https://reactjs.org/">React</a>, and site-builders like <a href="https://nextjs.org/">Next.js</a> and <a href="https://gatsbyjs.org">Gatsby.js</a></li><li><a href="https://vuejs.org/">Vue</a>, and site-builders like <a href="https://nuxtjs.org/">Nuxt</a>, <a href="https://gridsome.org/">Gridsome</a>, and <a href="https://vuepress.vuejs.org/">VuePress</a></li><li><a href="https://angular.io/">Angular</a>, and site-builders like <a href="https://github.com/scullyio/scully">Scully</a></li><li><a href="https://svelte.dev/">Svelte</a>, and site-builders like <a href="https://sapper.svelte.dev/">Sapper</a></li><li>Node.js based site builders like <a href="https://www.11ty.dev/">11ty</a> which offer a range of templating languages</li></ul><p>We are talking about some fairly large ecosystems here, and approaches like <a href="https://jamstack.org/">JAMstack</a>, which has taken the idea of generating websites from content over APIs to the modern web.</p><p>The authors of the web project book also offer some reflections on using PHP directly to generate output (as the worlds largest CMS does, Wordpress) and why you <em>wouldn&#x27;t </em>want to do that:</p><ul><li>Templating languages are “safer” in terms of not giving frontend developers powers to accidentally break the whole website</li><li>Templating languages are usually simpler</li><li>Generating output with full programming languages are “crude and unpleasant in most cases”</li></ul><p>There are some sweeping generalizations here that I suspect many front-end developers would disagree with. Using JavaScript along with a templating language in React or Vue gives you both pleasant ways of building the markup, but also pleasant ways of building interactivity and managing state. Interactive user experiences is often a demand, which involves having to deal with state. Following the author‘s advice, you often have to tack that on after the fact, which can lead to breakage and messy projects as well. </p><p>If you use a modern site-builder like Gatsby, it will, in most cases, catch errors and tell you when you&#x27;re rebuilding the site. And you will not be victim to closed database connections and rouge plugins typical for the CMSes that comes with built-in templating languages (yes, I know you can put caching layers onto those too). </p><p>There is no lack of testimonials of people that enjoy building the web using JavaScript, PHP (also using frameworks like Laravel), or whatnot. I don&#x27;t think it&#x27;s fair to say that it is “crude and unpleasant in most cases” anymore.</p><h2>Other Development Tasks</h2><h3>Users, Groups, and Permissions</h3><p>Although you can get pretty flexible access control, I prefer to emphasise trust and accountability when it comes to permissions for people who work with content. With great field description, and validation, and document actions, you can create affordances that remove bureaucracy while giving editors the safe-guards they need. </p><h3>Workflows and Content Operations</h3><p>Using features like Structure Builder and <a href="https://www.sanity.io/docs/custom-workflows">Document Actions</a> you can create different groupings and sortings of your document, based on everything from what they contain to kanban flows, or even user-specific document listings. </p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/5b646ee63942c8061a18a2eaf0084b6483f5d9a8-720x438.png?w=800&fit=max&auto=format" alt="Custom badges and custom document actions in Sanity Studio" /></figure><h3>Localization</h3><p>With Sanity, <a href="https://www.sanity.io/docs/localization">localization is an aspect of content modelling</a>, rather than its own dedicated feature. That&#x27;s a bit unusual, but when you take a closer look it makes sense: It means that you&#x27;re free to choose between field, document, or even dataset level localization (or a mix). It means you&#x27;re able to combine personalisation (segmentation) and localization relatively easily (if you think of it, they&#x27;re two sides of the same coin). </p><h3>Marketing Tools</h3><p>As mentioned above, you can write schemas that let you create specific content for specific groups or markets. You can use Sanity to handle routes on your website (or voice assistant or whatever), which can contain multiple versions of the same content and weights that your system for A/B-testing can use. You can also integrate with Mailchimp, Marketo, Hubspot, or Salesforce if you want to either push or pull content or data from those. </p><h3>Page Composition</h3><p>We tend to promote that you structure your content by not tying it to a specific presentation, but following the mental and operational models in your organization (hence, don&#x27;t organize it in <em>product pages</em>, but as <em>products</em>). Hence, page composition (or layout) should be a concern of the presentation layer, in most cases, the frontend. Then again, the ability to build landing pages of different modules and content types is a frequent request. Dean and Corey remark in a footnote, that:</p><blockquote>Dynamic page composition is exciting to see in a demo, but editorial teams usually never use it to the level they imagine they will.</blockquote><p>I suspect that they have a point, then again, if you have a productive marketing team, chances are that you are making landing pages to improve SEO and Adwords campaigns on a fairly regular basis. </p><p>With Sanity, you can have page composition, while still keeping it fairly structured. If you build with a design system and create components and modules with a decent level of abstraction. The usual pattern is to create an array-field, and compose dedicated object types to it. <a href="https://www.sanity.io/create?template=sanity-io%2Fsanity-template-nextjs-landing-pages">You can try this simple example on sanity.io/create</a>, where the field looks like this:</p><pre><code class="language-javascript">export default  {
  name: 'content',
  type: 'array',
  title: 'Page sections',
  of: [
    { type: 'hero' },
    { type: 'imageSection' },
    { type: 'mailchimp' },
    { type: 'textSection' },
  ]
}</code></pre><p>And here&#x27;s the field in actions, with an example front-end.</p><p><a href="https://youtu.be/91KRdixZjLY">Watch on YouTube</a></p><h3>Search, reporting, arching, integrations, and forms</h3><p>The authors also mention search, reporting, archiving, integration, and forms. With Sanity, you can choose to integrate with <strong>search</strong> services like Algolia and lets you customize search inside of the Studio. Using Structure builder you can set up lists with custom filters that let you get an overview (aka <strong>report</strong>) of unused assets, or content with <code>publishDate</code> from last year, or whatever you need. You can even use the <a href="https://www.sanity.io/blog/better-contentops-with-google-analytics-right-inside-the-sanity-studio">Google Analytics plugin to get a list of content with a high bounce rate</a>, in order words, actually use that data for something actionable.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/0071e7dcb6d79e182edd9532fd6d92fb976b12bc-720x405.png?w=800&fit=max&auto=format" alt="Google Analytics" /><figcaption>Google Analytics on Sanity Studio‘s Dashboard</figcaption></figure><p>Sanity keeps the patch&nbsp; history of your documents (like Google Docs) if you want to rewind and restore older versions. You can have an external <strong>archive</strong> by utilising the export endpoint if you prefer to delete documents, or you can add it as a boolean field and “soft delete” documents. <strong>Integrations</strong> are a huge topic, where the benefit of being real-time comes to shine: Set up services to path, augment and create content without having to deal with document locking on race-conditions. </p><p>If you need forms, you can do that too. Check out this <a href="https://github.com/sanity-io/netlify-form-sanity">example with Netlify</a>, or this <a href="https://github.com/NewFrontDoor/ui/tree/master/packages/form">implementation that let&#x27;s you use Sanity to also build forms that can be serialized and used in a frontend</a>. This is the power of having versatile APIs, and being a content platform, rather than the old CMS conventions but with APIs and an webapp you can&#x27;t customize as you need.</p><h2>The big picture</h2><p>I wholeheartly agree with Deane and Corey when they assert that “back-end and front-end implementations often run in parallel”. However, their following line isn&#x27;t necessarily true longer, at leats not with Sanity: “There’s a lot that a back-end team can do before they need the front-end team’s output for templating.“</p><p>If you have empowered your frontend-team to pick modern frontend technologies, they can start building at any time. With Sanity, they will not strictly need a “back-end team” to get the content APIs they need on day 1. Your frontend developers can use the same skills as they do with the frontend. In fact, if you put your developers, your designers, and your content people together in a two hour workshop, they should be able to be working with real content with Sanity Studio, while setting up the shell implementation of the presentation layer, and start sketching out which components and modules that needs to be designed. </p><p>In other words, it&#x27;s feasable to avoid having to coordinate “two teams”, but rather, have one team that works inter-displinary and focused on rapid and continous iteration. You still may need “back-end engineers” if you need to integrate with other services and back-office systems. They are usually happy because they don&#x27;t need to deal with the CMS and get powerful APIs that makes their work pleasant.</p><p>And this is not just consultant speak or my content marketing. I have been part of these processes using other systems, but also Sanity. Rather, it&#x27;s because this approach worke so well for me and my teams, I was happy to join Sanity.io to help other developers work better with content.</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/2058fd04d2f123e6a95baf42d3ed94145806df10-6000x4000.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>What vibe coding taught me about why I build</title>
      <link>https://knut.fyi/blog/2025-12-25/what-vibe-coding-taught-me-about-why-i-build</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-25/what-vibe-coding-taught-me-about-why-i-build</guid>
      <pubDate>Thu, 25 Dec 2025 19:03:32 GMT</pubDate>
      <description>I built a CLI tool in 40 minutes with Claude. It worked. People used it. But something felt off—and that discomfort revealed more about why I code than the code itself.</description>
      <content:encoded><![CDATA[<p>We were joking in the Sanity Slack about what it would look like if CLI tools had year-in-review features. You know, like Spotify Wrapped, but for your terminal.</p><pre><code class="language-">Knut 
06:42 i was hoping for Runtime Wrapped, @fil. Want to see how many recursions I caused.
06:43 "This is your year in stacks"
06:43 "Your favorite triggers are ['create', 'update']"

Fil 
06:43 can you imagine if your CLI tools did this kinda s**t

Knut
06:43 yes
06:43 i expect someone to

Fil
06:44 "you sent 1,300 POSTs with curl this past year!" f**k off
06:44 "you rage-typed sudo 100 times - you naughty boy!"

Magnus
06:44 "you still haven't learnt how to use tar I see"

Jack
06:44 npm wrapped: "you introduced 12,834 garbage transient
dependencies and caused your security team 14 different migranes"

Knut
06:45 knut from marketing here. i like this idea

Jack
06:45 :homer-disappearing-into-bushes:

Fil
06:45 ive made a terrible mistake</code></pre><p>Then I thought: I could build that.</p><p>40 minutes later, I had a working prototype. </p><p><a href="https://youtu.be/tNf_bhEaK9k">Watch on YouTube</a></p><p>A few hours after that, it had AI-powered roasts and shareable images. By the end of the day, it was on GitHub.</p><p>This is probably the best vibe coding experience I had so far. And I’m not sure how to feel about it. </p><p>Anyways. </p><h2>Introducing CLI wrapped</h2><p><a href="https://github.com/kmelve/cli-wrapped">CLI Wrapped</a> reads your shell history and shows you stats about your year in the terminal: top commands, time patterns, git activity, typos, and those moments where you ran a command, it failed, and you immediately tried again with <code>sudo</code>. </p><p>Optionally, You can add an API key and have Claude roast you for each statistic.</p><p>It&#x27;s a silly project. But the way I built it isn&#x27;t.</p><h2>Let&#x27;s clear up the magic trick</h2><p>When people hear &quot;vibe coding&quot; or &quot;AI-assisted development,&quot; I imagine they often picture someone typing &quot;make me an app&quot; and watching code appear. That&#x27;s not what happened here. That&#x27;s also not particularly useful for anything beyond throwaway prototypes.</p><p>What actually happened was more like pair programming with a very fast, very tireless partner who happens to know every library and pattern but needs you to make the actual decisions.</p><h2>The parts that stayed human</h2><p>Before I wrote any code - or asked Claude to - I had to think through:</p><ul><li><strong>What&#x27;s interesting about shell history?</strong> Not just &quot;most used commands&quot; - that&#x27;s boring. The fun is in the patterns: when do you code? What typos do you make? When do you rage-quit and try sudo?</li><li><strong>What should the experience feel like?</strong> I wanted it to feel interactive, one stat at a time, with personality. Not a wall of text.</li><li><strong>What about privacy?</strong> Shell history can contain secrets. If I&#x27;m sending anything to a third-party AI service for roasts, it can only be aggregate stats - command names and counts, never arguments or paths.</li><li><strong>What tech makes sense?</strong> Just copy the stack Claude Code uses. <a href="https://bun.sh">Bun</a> for speed and simplicity. <a href="https://react.dev">React</a> with <a href="https://github.com/vadimdemedes/ink">Ink</a> for terminal UI (because I know React). <a href="https://github.com/vercel/satori">Satori</a> for shareable images.</li></ul><p>None of these decisions came from <a href="https://claude.ai">Claude</a>. They came from thinking about what would make this actually fun to use. (But we all know Claude could probably have figured out that too).</p><h2>Claude&#x27;s actual job description</h2><p>Once I had the design in my head, I opened Claude Code (Opus 4.5) and described what I wanted. Not &quot;build me a CLI tool&quot; but something like: &quot;I want to parse zsh history files, extract timestamps and commands, and calculate stats like most-used commands, hourly patterns, and common typos.&quot;</p><p>Claude planned the implementation, I reviewed the plan, and then it executed. When something wasn&#x27;t right - and things weren&#x27;t always right - I&#x27;d describe the problem and we&#x27;d iterate. I was acting as the project manager, telling it about the user experience I wanted, security concerns, compatibility for different shells, how to make as easy as possible to run etc.</p><p>Claude is incredibly fast at the mechanical parts of programming. Parsing file formats, wiring up components, remembering the exact API for a library I haven&#x27;t used in months. The stuff that usually slows me down.</p><p>But it doesn&#x27;t know what would be <em>fun</em>. It doesn&#x27;t know that detecting rage-sudo moments is funnier than showing raw command counts. It doesn&#x27;t know that the roasts should be sarcastic but not mean. That&#x27;s taste, and taste is still human.</p><h2>When the font file was an error page</h2><p>It was actually the social image sharing feature where we had the most problems and it took some back’n’forth and tokens to resolve. </p><pre><code class="language-text">Error: Unsupported OpenType signature &lt;!DO</code></pre><p>That&#x27;s Satori trying to parse an HTML error page as a font file. Classic. I had no idea what was wrong, but Claude figured out the font URL was returning a 404. Debugging was still debugging. </p><p>At some point I had Claude build more testing so it could do more of the debugging loops itself. Should have just had it do that from the start. </p><h2>Real users find real bugs</h2><p>Within minutes of sharing the install link, a colleague ran it and got this:</p><pre><code class="language-text">Daily Activity:

 Sunday                          0
 Monday                          0
 Tuesday                         0
 Wednesday  ████████████████████ 20 ← Peak
 Thursday                        0
 Friday                          0
 Saturday                        0

🔥 Most active day: 2025-12-17 with 20 commands!</code></pre><p>His entire shell history was apparently 20 commands from that morning. Turns out his terminal was clearing history and he didn&#x27;t even know it. This led to adding a FAQ section about shell history retention and checking <code>~/.zsh_sessions/</code> for macOS users.</p><p>The first version I shared also broke interactive mode entirely when run via the curl installer. It worked fine locally. The TTY handling was wrong. These are the kinds of bugs that AI doesn&#x27;t magically prevent - you still ship broken things, you still get bug reports, you still fix them (or have the AI do it).</p><h2>What Claude thinks I learned</h2><p>I asked Claude to look back on this project and what we built and this is what it claims that I learned:</p><ol><li><strong>Design first, code second.</strong> The better I could articulate what I wanted, the better the results. Vague prompts got vague code. Doh.</li><li><strong>Iterate in conversation.</strong> When something wasn&#x27;t right, I&#x27;d describe why, not just what. &quot;This works but it&#x27;s not funny enough&quot; led to better solutions than &quot;change this.&quot; Doh.</li><li><strong>Know what you&#x27;re building.</strong> I could have built this without understanding React, terminal UIs, and API design. But I have a feeling that it turned out better because I did know things. So Claude amplified my knowledge; it didn&#x27;t replace it. Doh.</li><li><strong>Ship fast, then polish.</strong> The first version had bugs. I shipped it anyway, got feedback, and fixed things. The speed made that iteration loop possible. Doh.</li></ol><p>All of these are true, and… bleeding obvious. Principles that have been rehearsed many times.</p><h2>The knowledge that didn&#x27;t stick</h2><p>I don’t really know how to build CLI apps with Ink. I’m not better at hand coding images with React and Satori either. </p><h2>What I actually learned</h2><p>There’s one I thing that I actually learned, that Claude didn’t catch (well, how could it, since I didn’t tell it).</p><p>I did learn how to parse the history of different shells though, and how to change the retention. This was the part of the program I felt I had to review and understand to make sure it actually didn’t pass sensitive information </p><h2>Velocity as feature</h2><p>What made this different from side projects pre AI tools actually becoming good?</p><p>Normally, I&#x27;d have the idea, think &quot;that would be cool,&quot; and then reality would set in. I&#x27;d remember I don&#x27;t know the zsh history format off the top of my head. I&#x27;d have to learn how to build with Ink. I&#x27;d spend an hour just on setup. By the time I got to the interesting parts, my enthusiasm would be gone and I&#x27;d have other things to do.</p><p>With Claude, the gap between &quot;I have an idea&quot; and &quot;I can see if the idea is good&quot; collapsed. 40 minutes to a working prototype means I could validate the concept while I was still excited about it. </p><p>I think?</p><p>On the one side it’s fun to move at the speed of your ideas instead of the speed of your debugging.</p><p>But on the other side, I don’t feel super attached to this creation. </p><h2>Your turn to feel weird about it</h2><p>Here&#x27;s the thing I didn&#x27;t expect: I did make something. It works. People ran it, found bugs, I fixed them. That&#x27;s real.</p><p><strong>And yet I kept circling back to that feeling: not quite satisfaction, not quite disappointment. Something in between.</strong></p><p>I realized: I build things because I learn through building them. Not just &quot;how to use a library&quot; learning, but the deeper kind: understanding why something matters, what makes it work, where the edges are. The kind of knowledge that mostly comes from wrestling with a problem yourself.</p><p>Vibe coding compressed that loop. I got the thing without the wrestling. And in doing so, I learned something I couldn&#x27;t have learned any other way: that the wrestling is often the point.</p><p>The irony isn&#x27;t lost on me: I learned this through vibe coding. I did learn how to use Claude Code better. I did learn about shell history formats and TTY handling, the parts I had to understand to trust the output. The tool taught me something by showing me what it couldn&#x27;t teach me.</p><p>So is this good or bad? I don&#x27;t know. It&#x27;s fast. It&#x27;s useful. It works. But it also reveals something uncomfortable about what we value in the act of creation and I&#x27;m not sure we&#x27;ve figured out what to do with that yet.</p><h2>Dare try it yourself</h2><p>If you want to see what your terminal year looked like. Just uncritically execute this code from the Internet in your terminal:</p><pre><code class="language-bash">bash &lt;(curl -fsSL https://raw.githubusercontent.com/kmelve/cli-wrapped/main/install.sh)</code></pre><p>The code is on <a href="https://github.com/kmelve/cli-wrapped">GitHub</a>. </p><p>You should probably read through it (or have Claude do it) first. </p><p>And tag me (<code>@kmelve</code>) on social if you do! </p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/ca26f73d1eb16415ebe44b2f5f4f3e7f8260407c-1200x630.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Getting started with Webmentions in Gatsby</title>
      <link>https://knut.fyi/blog/2025-12-20/getting-started-with-webmentions-in-gatsby</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/getting-started-with-webmentions-in-gatsby</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>Let&apos;s learn how to implement Webmentions friendly markup, and get them onto your website made with Gatsby. Bonus: You&apos;ll also learn how to trigger new builds on Netlify whenever a mentoin happens. </description>
      <content:encoded><![CDATA[<p>I have been curious to learn more about webmentions and the <a href="https://indieweb.org">IndieWeb</a> for a while now. Putting together my new blog seemed like an excellent opportunity to learn more about it. So keep in mind that I’m pretty new to this stuff, and just sharing my learning process as I go along. This is at least a short tutorial to how to get started with making your site webmentions friendly, how to connect them with twitter, start retrieving them with <a href="https://www.gatsbyjs.org">Gatsby</a>, and how to trigger rebuilds on <a href="https://netlify.com">Netlify</a> when new mentions come in. I’ll revisit how to send them in a later tutorial.</p><p>I got started on my webmentions journey by reading Chris’ <a href="https://www.christopherbiscardi.com/post/building-gatsby-plugin-webmentions"><em>Building Gatsby Plugin Webmentions</em></a> and Chad’s <em><a href="https://www.chadly.net/embracing-the-indieweb/">Embracing the IndieWeb</a>. </em>Both works were helpful to get started, but they left some details out that may have made it a bit easier to grok. I’ll walk you through all the steps, but do check out their stuff as well.</p><h2>What is Webmentions?</h2><p>You can read more about it on the <a href="https://indieweb.org/Webmention">IndieWeb wiki</a>, but shortly put: Webmentions is an open source project and a service to send and receive messages and pingbacks between sites. Like we all did with Wordpress back in the day.</p><p>The difference are that Webmentions are federated, meaning that you can collect and send mentions from multiple sources. In this tutorial, I’ll start by pulling in twitter mentions via a service called <a href="https://brid.gy">brid.gy</a>.</p><h2>How to get started with Webmentions</h2><p>To get started with Webmentions you need to sign in on webmention.io. And to sign in you need to authenticate. And to authenticate you need to put some markup on your Gatsby site that tells webmention.io which service it can use. As per instructions you can add the following using either Twitter, GitHub, email, your PGP key, or you own IndieAuth server. I added Twitter and Github:</p><pre><code class="language-html">&lt;p&gt;
  Follow &lt;a class="h-card" rel="me" href="https://www.knutmelvaer.no/"&gt;Knut&lt;/a&gt; on &lt;a href='https://twitter.com/kmelve' rel='me'&gt;Twitter (@kmelve)&lt;/a&gt;, &lt;a href='https://github.com/kmelve' rel='me'&gt;GitHub&lt;/a&gt;, or send him an &lt;a class="u-email" href='mailto:knut.melvaer@gmail.com' rel='me'&gt;email&lt;/a&gt;
&lt;/p&gt;</code></pre><p>So this pretty much looks like your regular piece of HTML. If you look a little closer there is some <code>rel=&quot;me&quot;</code> attribute as well as some class names (<code>h-card</code>, <code>u-email</code>). These are microformats (TK), which is an important part of getting webmentions to work. When you publish your site with this markup, you tell webmention that those social accounts are tied to whomever is control of the domain the site is on, and lets you log in via their oAuth-integrations.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/cdd3356565f50c8c9ce5301a409ee336406dcf8f-1071x874.gif?w=800&fit=max&auto=format" alt="Screencast showing how to login to webmention.io with twitter" /><figcaption>The login flow on webmention.io</figcaption></figure><p>As you can see in the figure above, I have a list of webmentions there that you probably don’t have (yet). We’ll return to how to get that list populated with stuff from twitter, but first, we have to add some microformats to our site, to make it webmention friendly.</p><h2>Adding microformats2 to your posts</h2><p>Webmentions use a specification called <a href="http://microformats.org/wiki/h-card">microformats2</a> to make sense of contents on a webpage. We’ve already started implementing it in the code snippet above. There’s a lot to microformats that I haven’t unpacked for myself yet, but it’s easy enough to get started. You mainly do so by adding some specified class names to HTML elements that have the specific content that webmention use to populate its fields. For example:</p><pre><code class="language-html">&lt;article class="h-card"&gt;
  &lt;header&gt;
    &lt;img class="u-photo" src="http://..."&gt;
    &lt;h1 class="p-name"&gt;The Title&lt;/h1&gt;
  &lt;/header&gt;
  &lt;p class="p-summary e-content"&gt;The summary&lt;/p&gt;
  &lt;footer&gt;
    &lt;a class="u-url p-name" href="http://..."&gt;The author&lt;/a&gt;
  &lt;/footer&gt;
&lt;/article&gt;</code></pre><p>You can use <a href="https://indiewebify.me/">IndieWebify.me</a> or <a href="https://pin13.net/mf2/">pin13.net</a> to validate your markup. I took a couple of tries for me to get it right. When a webmention service read your page, it will parse the HTML and extract this information into a JSON structure, that may look something like this:</p><pre><code class="language-json">{
  "items": [
    {
      "type": [
        "h-card"
      ],
      "properties": {
        "name": [
          "The Title",
          "The author"
        ],
        "summary": [
          "The summary"
        ],
        "photo": [
          "http://..."
        ],
        "url": [
          "http://..."
        ],
        "content": [
            {
              "html": "The summary",
              "value": "The summary"
            }
        ]
      }
    }
  ],
  "rels": {},
  "rel-urls": {}
}</code></pre><p>I ended up implementing these “microformated” elements in my post template and hiding them with <code>display: none</code>. Mainly because I didn’t want an <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO8601 formatted datetime</a> stamp visible on the site. I could probably have used a library like <a href="https://date-fns.org/">date-fns</a> to format the timestamp, but this did the trick without any dependencies. This is for example how it looks in my Gatsby blog’s React code:</p><pre><code class="language-jsx">&lt;time 
  className={styles.hidden + " dt-published"} 
  itemprop="datepublished" 
  datetime={publishedAt}
&gt;
  {
    new Date(publishedAt)
      .toISOString()
      .replace('Z', '') + "+01:00"
  
  }
&lt;/time&gt;</code></pre><p>Now, let’s head over to the interesting part, namely, getting webmentions into Gatsby.</p><h2>Installing <code>gatsby-plugin-webmention</code></h2><p>The easiest way to get webmentions into a Gatsby site is to install the gatsby-plugin-webmention plugin:</p><pre><code class="language-">npm install gatsby-plugin-webmention
# or
yarn add gatsby-plugin-webmention</code></pre><p>Now you can add the following config to the <code>plugins</code> array in <code>gatsby-config.js</code> (obviously replacing my information with your own):</p><pre><code class="language-javascript">{
  resolve: `gatsby-plugin-webmention`,
  options: {
    username: 'www.knutmelvaer.no', // webmention.io username
    identity: {
      github: 'kmelve',
      twitter: 'kmelve' // no @
    },
    mentions: true,
    pingbacks: true,
    domain: 'www.knutmelvaer.no',
    token: process.env.WEBMENTIONS_TOKEN
  }
}</code></pre><p>The webmentions token is the one that you find under &quot;API key&quot; when you’re logged into <a href="https://webmention.io/settings">https://webmention.io/settings</a>. Remember to also add it to the environment where you build your Gatsby site to production (<a href="https://www.netlify.com/docs/continuous-deployment/#environment-variables">for example on Netlify</a>). If all went well, you’ll be able to query your webmentions in the Gatsby GraphQL API. </p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/d0993e90b5e5cb536f054433a3f96d81f364f873-2642x1606.png?w=800&fit=max&auto=format" alt="Example of a webmentions query in Gatsby’s GraphiQL" /><figcaption>Example of a webmentions query in Gatsby’s GraphiQL</figcaption></figure><p>In order to get page specific webmentions I did two things:</p><ol><li>Generate and put the post’s URL into <a href="https://www.gatsbyjs.org/docs/creating-and-modifying-pages/#pass-context-to-pages"><code>context</code></a> from <code>gatsby-node.js</code></li><li>Filter the allWebMentionEntry with the URL aka &quot;the permalink&quot;</li></ol><p>There’s probably a handful of ways to do this, but I ended up with just generating the full URL in gatsby-node.js, and passing it in via context, so that I could use it as a query param:</p><pre><code class="language-javascript">postEdges
  .filter(edge =&gt; !isFuture(edge.node.publishedAt))
  .forEach((edge, index) =&gt; {
    const { id, slug = {}, publishedAt } = edge.node
    const dateSegment = format(publishedAt, 'YYYY/MM')
    const path = `/blog/${dateSegment}/${slug.current}/`

    createPage({
      path,
      component: require.resolve('./src/templates/blog-post.js'),
      context: { 
        id, 
        permalink: `https://www.knutmelvaer.no${path}`
      }
    })

    createPageDependency({ path, nodeId: id })
  })</code></pre><p>And the GraphQL query:</p><pre><code class="language-json">allWebMentionEntry(filter: {wmTarget: {eq: $permalink}}) {
  edges {
    node {
      wmTarget
      wmSource
      wmProperty
      wmId
      type
      url
      likeOf
      author {
        url
        type
        photo
        name
      }
      content {
        text
      }
    }
  }
}</code></pre><p>The properties of this query will be pretty self-explanatory when you start to get webmentions data. You can use it to list people who have liked, replied, or reposted your post. </p><p>The simplest way to get some webmentions going is to use a service called brid.gy to bring in mentions of your website on Twitter. </p><h2>Connecting brid.gy</h2><p>Head over to <a href="https://brid.gy">brid.gy</a> and connect your accounts, I think Twitter makes the most sense, at least at first. Enable the listening for responses. There need to be some tweets that mention your site (by domain name) to there being responses. You can, of course, just tweet your self to get something going. </p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/b7e4f7cbe80c2aee6a56866248f41e49c8d3e937-2588x1706.png?w=800&fit=max&auto=format" alt="The brid.gy interface listening for responses." /><figcaption>The brid.gy interface</figcaption></figure><p>If you (re)start your Gatsby site in dev mode, you’ll be able to see the same response data in your GraphQL layer. This will make it a bit easier to implement in your frontend template. </p><h2>Implementing webmentions in your Gatsby frontend</h2><p>I’m not going to cover much detail here, this is the creative part! It’s pretty much straight forward though. For example, to filter out all the &quot;likes&quot; and show some avatars with links to the &quot;liker&quot;, you can do something along these lines (not saying that this is the definitive way to do it):</p><pre><code class="language-jsx">import React from 'react'

export default function WebMentions ({ edges }) {
  const likes = edges.filter(({ node }) =&gt; node.wmProperty === 'like-of')
  const likeAuthors = likes.map(
    ({ node }) =&gt; node.author &amp;&amp; { wmId: node.wmId, ...node.author }
  )
  return (
    &lt;div&gt;
      &lt;h4&gt;
        &lt;span&gt;{`${likes.length} likes`}&lt;/span&gt;
      &lt;/h4&gt;
      &lt;div&gt;
        {likeAuthors.map(author =&gt; (
          &lt;a href={author.url}&gt;
            &lt;img alt={author.name} src={author.photo} key={author.wmId} /&gt;
          &lt;/a&gt;
        ))}
      &lt;/div&gt;
  )
}
</code></pre><p>You can use this component where you query for webmentions, by sending the <code>allWebMentionEntry</code> object into it <code>&lt;WebMentions {...allWebmentionEntry} /&gt;</code>. </p><h2>Triggering a new build on a new mention</h2><p>If you’re like me, you’re impatient and want new mentions to appear when they happen. If you are patient, you can be satisfied by having the new mentions appear whenever you rebuild your site. But if you host your Gatsby site with let&#x27;s say Netlify, you can use a build trigger to automatically rebuild the site, querying the newest mentions. First, you’ll have to add a new <a href="https://www.netlify.com/docs/webhooks/#incoming-webhooks">build trigger</a> on Netlify. Copy this, and head over to <a href="https://webmention.io/settings/webhooks">the webhooks settings</a> on Webmentions. Paste the Netlify URL into the box (no secret is needed), and hit save. And that’s it! (I realize that we can do some interesting things with this webhook, but we’ll revisit that in a later post.)</p><p>I would also recommend setting up some build notifications on Netlify so that you can keep an eye. Especially if you’re actually putting some content from the webmentions on to your site. This would be the time I told you that you can delete webmentions, and add someone to your blocklist if this is needed. Hopefully, it will not though. </p><h2>Congratulations, you’re now a bit more indie!</h2><p>There’re still some pieces left to the puzzle. We haven’t set our site to send webmentions or pingbacks yet. And there are more sources than Twitter that we can draw from. I’ll surely return with more fun IndieWeb + Webmentions stuff though. Meanwhile, feel free to reply to me on twitter, or even try webmentioning this post if you have constructive feedback. I’ll happily amend this post and follow up with useful insights.</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/bf688e27ee1d0cda9c960aebf179f51fb0440c5d-5343x2987.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>On the limits of MDX</title>
      <link>https://knut.fyi/blog/2025-12-20/on-the-limits-of-mdx</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/on-the-limits-of-mdx</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>Should you lock your content into JSX infused Markdown? Probably not. Here are some reasons why MDX has its limits.</description>
      <content:encoded><![CDATA[<p>I feel I must preface this with some hedging because I&#x27;m about to challenge something that many people seem to love: MDX.</p><p> I have nothing but respect for those who contribute to the MDX ecosystem. Also, I&#x27;m totally the type of person that would love MDX. I have been writing in Markdown since 2004, and one of my first GitHub projects was a <a href="https://github.com/kmelve/WordtoMMDfootnotes">jQuery based markdown-footnotes plugin for Wordpress</a> (jeez louise don&#x27;t use this!). At university, I had a whole multimarkdown-to-LaTeX setup in <a href="https://www.sublimetext.com/">Sublime Text</a> with <a href="https://pandoc.org/">pandoc</a>, <a href="http://www.bibtex.org/">BibTeX</a>, and PDF preview with <a href="https://skim-app.sourceforge.io/">Skim</a> going for me. It was kinda great (at least for the two weeks the setup worked)</p><p>I don&#x27;t think MDX should be “considered harmful”, nor that everyone should stop using it. But I think there are some things worth considering before locking your, or rather, others’ content to it. And I&#x27;m writing this knowing there might be things I&#x27;ve missed or not considered. Feel free to respond to me with your own blog post, or on <a href="https://twitter.com/kmelve">twitter</a>. I don&#x27;t think this is <a href="https://www.urbandictionary.com/define.php?term=Not%20a%20hill%20I%20want%20to%20die%20on">the hill I want to die on</a> though. So I&#x27;ll allocate no more than 3 hours to follow up on this discussion. Use them wisely.</p><p>With that out of the way. Let&#x27;s jump into this. 🏊</p><h2>What is MDX? </h2><p>If you go to <a href="https://mdxjs.com">mdxjs.com</a> it self-defines as “an authorable format that lets you seamlessly write JSX in your Markdown documents.” For those not in the know, JSX is “an XML-like syntax extension to ECMAScript without any defined semantics.“ (at least as proposed by the <a href="https://facebook.github.io/jsx/">draft specification</a>). In order words, MDX, that is, the MDX precompiler, lets you combine the templating syntax usually found in React.js projects with Markdown. It looks something like this:</p><pre><code class="language-markdown"># Hello, *world*!

Below is an example of JSX embedded in Markdown. &lt;br /&gt; **Try and change
the background color!**

&lt;div style={{ padding: '20px', backgroundColor: 'tomato' }}&gt;
  &lt;h3&gt;This is JSX&lt;/h3&gt;
&lt;/div&gt;
</code></pre><p>It may look like HTML, because it does, but it&#x27;s JSX. The intriguing part with MDX, but also the… uhm… problematic part, is that you can do stuff like this:</p><pre><code class="language-markdown">import { Button } from './Button'

# Hello world

Hello, I'm still a mdx file, but now I have a button component!

&lt;Button&gt;Click&lt;/Button&gt;</code></pre><p>(example from the <a href="https://www.docz.site/docs/writing-mdx">docz.site</a>)</p><p>Yep, you can import JSX components and embed them with your run-of-the-mill Markdown prose. If you&#x27;re documenting your JSX based component library, which is what Docz let you do, this makes all the sense in the world. MDX is also used to author slide decks in <a href="https://github.com/jxnblk/mdx-deck">mdx-deck</a>, which is very appealing if you&#x27;re tired of clicking around in Keynote/PowerPoint/Google Sheets. Which many of us are. I&#x27;m not denying the appeal or usability of MDX for certain things for certain people.</p><p>From a React developer’s standpoint that it&#x27;s used to writing JSX, MDX seems to be touching on the ethos of Markdown, at least as John Gruber, it&#x27;s original creator, introduce it on <a href="https://daringfireball.net/projects/markdown/">daringfireball.com</a>:</p><blockquote>Markdown is a text-to-HTML conversion tool for web writers. Markdown allows you to write using an easy-to-read, easy-to-write plain text format, then convert it to structurally valid XHTML (or HTML).</blockquote><p>Markdown has always allowed inline and block-level HTML to express things outside of the syntax. Because HTML was the end product. In that way, MDX isn&#x27;t much different. Markdown&#x27;s key feature though is &quot;easy-to-read, easy-to-write&quot;. I&#x27;m not sure if MDX keeps within, or moves away from this general intent.&nbsp; Gruber made a syntax that was easier to read and write for anyone not familiar with HTML. I&#x27;m not convinced that JSX solves the same problem. </p><h2>What is the problem MDX tries to solve?</h2><p>Markdown was designed at a time where most of the web authoring was still done in HTML. It was also designed when web content was mostly text and images. This isn&#x27;t the case anymore. Web content has moved towards a much richer set of components, from embeds to interactive codeblocks, to between-paragraphs call to actions.</p><p>MDX seems like an attempt to make these components available to the author in the same syntax used in frontend development (well, as long as your frontend development uses JSX). And that&#x27;s pretty much it. I think.</p><p>But this problem has been solved already. With something they call “rich text editors.”</p><p>There&#x27;s plenty of content platforms with plenty of rich text editors that spew out plenty of different formats, including markdown, HTML, and abstractions as <a href="https://github.com/bustle/mobiledoc-kit">MobileDoc</a> and <a href="http://portabletext.org/">Portable Text</a>. Medium gained popularity thanks to its smooth authoring experience, <a href="https://notion.so">Notion</a> now seems to have taken over that hype. Void of HTML and Markdown (well, markdown-like shortcuts works, but is not a requirement), but with rich embeds. Arguably, these interfaces are more friendly and more accessible than learning Markdown, or MDX.</p><p>There had to be at least one reason for Slacks introduction of a rich text editor, yes, <a href="https://www.vice.com/en_us/article/pa7nbn/slacks-new-rich-text-editor-shows-why-markdown-still-scares-people">it wasn&#x27;t very well executed</a>, and we got Markdown back (I actually got used to the RTE), but I suspect they actually attempted to solve real user experience problems: <em>Not everyone wants to learn Markdown</em>.</p><h2>Hey, I&#x27;m writing here!</h2><p>I have written React for 20 years (that&#x27;s recruiter for “since 2015”). I should know how to use my keyboard to paint beautiful JSX components with some lovely props and all that. But for some reason, when I have been made to write MDX. It. just. doesn&#x27;t. work. I mess the syntax up all the time. Forget that I can&#x27;t actually be writing Markdown inside of an MDX component (without wrapping it in some MDXprovider something). No syntax highlighting (this may have changed at the time you read this). No helpful error to actually point out where I forgot to close that component. Yeah, I know, but I was really supposed to be writing. Not doing debugging of frontend code. </p><p>And yeah… speaking of those components. Most times I had to use MDX, it was to contribute to someone else&#x27;s documentation. So that means that I had to actually look up a bunch of documentation just to make a code example or a “note”. Sure, I could just <a href="http://www.locusmag.com/Features/2009/01/cory-doctorow-writing-in-age-of.html"><em>TK&#x27;ed</em></a> those parts (and I did), but again, it feels unnecessary for doing something that could be seamlessly solved with a text editor.</p><p>This is my totally subjective experience, but for now MDX is introducing a level of friction that I&#x27;m not really ok with when I&#x27;m writing. This takes me to the next section. Other people!</p><h2>So, are we expecting people to use this?</h2><p>I generally have hesitations dividing people into <a href="https://knut.fyi/blog/2025-12-20/who-are-the-non-technicals">“techies” and “non-techies”</a> (I can be persuaded if you actually identify as a <a href="https://en.wikipedia.org/wiki/Luddite">Luddite</a>). But I have been through enough projects as a consultant and have been through enough user tests to be very careful in forcing even Markdown on people who go to work to do content. <strong>Writing with a markup syntax should be opt-in, not forced upon you.</strong> </p><p>You expect a person who probably already have too much stuff to do, to:</p><ol><li>Learn Markdown</li><li>Then learn MDX/JSX and imports</li><li>Internalize your component system (that never changes, right?)</li><li>Work with plain files</li><li>Use git or whatnot to actually get the stuff where it needs to go</li><li>Ask you how to troubleshoot their texts <em>when </em>it gets borked</li></ol><p>Sure, you have managed to persuade your client to do it and that&#x27;s jolly good. But I know that for most people that don&#x27;t share our coding interests, this will not fly. Not the bit. Also, you&#x27;re asking them to put their content into a certain format that arguably marries it to presentation. That&#x27;s probably OK for a slide deck, but less OK if that content is actually describing something of value inside of your organization. </p><p>And it has nothing to do with people being &quot;technical&quot; or not. Most content people I know can spot an apostrophe from a grave accent after two jaegers after a seminar. They know how the syntax works. At least some of the time. It&#x27;s about what we can reasonably expect people to have to deal with. Should they be learning to write JSX components, when frankly, that&#x27;s your job?</p><p>“But Knut, I have this client and they love it”. Sure, that&#x27;s great for you and your client! But now you have another challenge. That client may want their content elsewhere. Or well, redesign their site the year we all do everything in WebGL. Or they just want to switch out their design system with new components. Yes, I know you have an <a href="https://mdxjs.com/advanced/ast">AST</a>. But you know what&#x27;s better than an AST? Not to have to use an AST. </p><p>Because it&#x27;s not only moving between formats and languages, it&#x27;s also how you actually structure your content by what it means, and not after the whims of a specific presentation.</p><h2>You can&#x27;t unmix cake</h2><p>I work for a <a href="https://www.sanity.io">company that promotes structured content</a>, so you should see this coming from a mile away:</p><p><em>For most uses of MDX, you will end up mixing specific presentation concerns with your content. This is not great.</em></p><p>Yeah, it kinda worked for HTML. Until something called iOS came along. And then you had an icky problem. Sure, you could parse it. But have you ever tried to <a href="https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags">parse real-world HTML</a>? You probably rather spend your afternoon on something different. </p><p>For people working with content strategy, content is best stored as ingredients from which you can bake the things that you need when you need it. They have been preaching “structured content” for ages and fighting with CMSes that force content into WYSIWYG page builders and make editors copy-paste their texts around in small layout boxes prisons. </p><p>Yes, technically you can be really semantic with MDX too. Compose your components to be great meaningful abstractions, not get tempted to use that <code>style</code> attribute, and keep everything neatly separated in their own documents. But there&#x27;s little in the design and practice of MDX that promotes this use. It is promoted as a way to build rich visual presentations. </p><p>Sometimes you want to make a cake, and that&#x27;s fine. But you should think really hard if you could feed a lot more people for a lot less effort if you hadn&#x27;t made the cake in the first place. Ok, this metaphor is pretty tired now. The point is: You should think really hard about how you want to be able to work with your content, the inclinations of whom you want to work with your content, and finally, how sustainable and flexible your structuring of it is.</p><h2>The obligatory section where I try to sell you our thing</h2><p>I get it. I get the tangibility of flat files. I get that it feels good to take your coding skills into your prose. But it&#x27;s not the best way to work with content. Text editors with familiar affordances that produce typed rich text that can be queried and serialized into whatever you need are better. Where developers can define the data structures they need, and editors get easy-to-use tools to get their work done. Like what we&#x27;re building at <a href="https://www.sanity.io">Sanity</a> with <a href="https://www.portabletext.org">Portable Text</a>. </p><p>But it doesn&#x27;t even need to be Sanity. After we launched with Portable Text, others have recognized that storing rich text in JSON structures has its advantages. No, you will never want to actually read or author the JSON, but you shouldn&#x27;t need to. That what&#x27;s React and JSX are best for. Namely, building the editorial experiences that don&#x27;t come in the way of writing. That can be reused across frameworks, programming languages, and redesigns.</p><h2>Closing remarks</h2><p>(take a minute to appreciate <a href="https://github.com/mdx-js/mdx/tree/master/packages/remark-mdx">that subtle pun</a>)</p><p><em>With that out of the way</em>, let me reiterate that I don&#x27;t want to knock the people behind MDX and adjacent technologies. It obviously brings some people joy. You shouldn&#x27;t feel bad for using it either, but now at least you have some counterpoints to make better decisions from. Maybe someone gets inspired to prove me wrong and make a structured content pattern library for MDX. That would at least be something. Or use some of my aforementioned allocated three hours of discussion time to tell me everything that&#x27;s wrong with the Portable Text specification (I welcome it actually if it can make it better).</p><p>But do make sure you have considered if MDX solves the problem you really should be solving, or if it only tickles your developer fancy.</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/3dde9b014a131f3de5b0a7dd151a4e96fae63578-597x400.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>This blog runs on public code, without secrets</title>
      <link>https://knut.fyi/blog/2025-12-20/this-runs-on-public-code-without-secrets</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/this-runs-on-public-code-without-secrets</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>I made the code for this blog public, and I removed all accidental secrets from the git history.</description>
      <content:encoded><![CDATA[<p>I have kept the code that powers this blog in a private <a href="https://github.com/kmelve/knutmelvaer-no">GitHub repository</a> for a while. Partly because I wasn&#x27;t super proud of my code, but also because I wasn&#x27;t sure if I had put something in it that wasn&#x27;t for public consumption, that is, secret tokens.</p><p>Since people kept asking me about how I did the <a href="https://knut.fyi/blog/2025-12-20/getting-started-with-webmentions-in-gatsby">webmentions stuff</a>, and the twitter stuff, and the statistics stuff, I decided to make it public. So I did clean up the code a bit and added <a href="https://github.com/kmelve/knutmelvaer-no/blob/master/README.md">a disclaimer in the README.md</a>.</p><p>To deal with the “there might there be secrets” situation, I used a handy little command line tool called <a href="https://rtyley.github.io/bfg-repo-cleaner/">BFG Repo-Cleaner</a>. It goes through all your commits in all your branches and rewrites history to not the include the file you wish you hadn&#x27;t commited:</p><pre><code class="language-text">bfg --delete-files .env</code></pre><p>Afterwards you can <code>git push --force</code> to overwrite the history stored on GitHub.</p><p>To install BFG, you can follow the installation instructions, or if you are on macOS and have Homebrew installed (as you should), you can run:</p><pre><code class="language-text">brew install bfg</code></pre><p></p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/792b43c52fdbef23f6645f80fcad3ac334133fc5-3630x2420.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>2019 – A personal retrospective</title>
      <link>https://knut.fyi/blog/2025-12-20/2019-a-personal-retrospective</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/2019-a-personal-retrospective</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>A reflection on the past year, and some goals for the new one.</description>
      <content:encoded><![CDATA[<p><a href="https://lengstorf.com/2019-personal-retrospective/">Jason’s personal retrospective</a> inspired me to look back at 2019 in order to gauge where I have been and where I want to be at the end of 2020. It seems useful and mildly interesting to have a record of the high-level stuff you have been occupied with and establish some goals. It’s also an opportunity to be open about ambitions and reflect on some of the struggles that come with being human.</p><h2>Where was I at the end of 2018?</h2><p>Since I didn&#x27;t write a retrospective last year, I&#x27;ll have to construct one. At the end of 2018, I was looking back at a year where I moved from the western to the eastern part of the country, close to Oslo, Norway’s capital city. I had also changed jobs, ending my three-year tenure as a technology/UX consultant at <a href="https://www.netlife.com">Netlife</a>, and starting as Head of Developer Relations at <a href="https://sanity.io">Sanity.io</a>. It was the year where I finally sought professional help for my recurring depressions and got <a href="https://www.instagram.com/p/BndPVcJAXJZ/">a new Whippet puppy</a>.</p><h2>How did I do in 2019?</h2><p>I didn’t blog any goals in 2019, but there were some that emerged during the year. It became increasingly clear to me that I needed to improve my work-life balance, invest more in my personal relationships, and be more structured and focused on how I approach my work.</p><h3>Work-life balance</h3><p>I think of this <em>a lot</em>. And frankly, it’s complicated. I think we shouldn’t expect people to put in more than 40 hours a week, while I often do work more myself because I enjoy it (not because it’s expected of me). Setting boundaries is important though. During 2019 I have managed to develop the habit where I mostly manage to take weekends and holidays completely off in terms of typing on keyboards. Since a lot of my work involves staying on top of social media accounts, and the developer community, it’s hard to stay completely out of the loop, so I’ll still be checking in more or less frequently, but not participating. Deleting Slack from my phone and keeping my laptop closed and out of the way have been effective measures keeping me from “accidentally working” during my vacations. During longer periods of spare time, I keep catching up to a minimum, once in a day or a few.</p><p>On the other side: It’s fun and exciting to work at a startup like Sanity.io, especially because I thrive on learning new things and putting out stuff. There’s also always something to do. And even though I’m regularly reminded to take time off and not work too much, it’s hard not to when you’re motivated. There’s no lack of advice against burnout out there, and having previously experienced it in my past life as a Ph.D. student, I’m well aware of the mechanics. At the same time, I’m super privileged working with something I enjoy every day, and in a welfare state that gives me a lot of existential security.</p><h3>More focus, more structure</h3><p>A big change for me in 2019 is how I approach creative work. Previously, the process for me has usually been coming up with something based on some inspiration or idea that hit me, acting on it immediately, and getting it out. I was afraid that if I waited to act on an impulse and not get it out relatively fast, it would wither away. Although this approach has in many cases worked very well for me, it doesn’t scale well. Especially if you’re trying to do stuff more strategically with other people.</p><p>So the two things I have practiced a lot and got better on during 2019 are <em>planning</em> and <em>prioritization</em>. An important note is that I don&#x27;t do this to be more productive in terms of more output, that was never the problem, but because I wanted to spend my time on the <em>right things</em> (or the things we wanted to check were the right things). In a way, more focus and more structure will enable me to work less and get the same amount of things done. </p><p>I really got to put my goals to the test when we planned a pretty hefty release schedule for November and December. If I hadn&#x27;t been diligent with planning and prioritization, I wouldn&#x27;t be able to get stuff done. The proof is in the eating (and also making) of the pudding, as they say.</p><h3>Mental health and personal relationships</h3><p>I also spent 2019 trying to take better care of my own mental health and paying more attention to my personal relationships. It hasn’t been just smooth sailing. I have lived with recurring depressions pretty much my whole adult life and it took a long time before I recognized what was going on. After I sought help I have been able to more quickly identify when depression hits, and I have been able to curb a lot of the behavior that comes with it. Cognitive Behavioural Therapy has worked for me, but it’s only one way to go about it. I started going to a therapist again. I’m generally doing fine, but I don’t want to get blindsided by depression again.</p><p>I have started dedicating more time and attention to the people around me: family, and friends. I realized how I avoided contact because of my own insecurities, and how I haven’t really been present when I have been around them. In 2019, I actively sought out contact and practiced being more present, open, and forthcoming. Although this has been generally a positive turn, I have also had to work with some difficult stuff in my close family that is still unresolved. I guess that will spill over as a goal for 2020.</p><h2>What’s up for 2020?</h2><p>I don’t think I’ll set some big life-changing goals for 2020, but rather, continue working with the goals that emerged in 2019. </p><ul><li><strong>Work-life balance.</strong> Take care of our horses once a week (my partner does it for the rest of the week). Plan more hiking trips during summer. Uphold a minimal physical exercise regime. Do more with friends and colleagues outside of work.</li><li><strong>More focus, more structure</strong>. Practice saying “no” more. Be more diligent about putting plans to paper. Write more about what I learned about how to work (in order to “slow-think” it).</li><li><strong>Mental health and personal relationships</strong>. Practice recognizing negative thoughts and identifying techniques to prevent them. Be more open about how I’m actually doing to those close to me. Prioritize time with family and figure out how to be an uncle.</li></ul><p>Looking at what I ended up writing, it hits me that all this is probably pretty demographically typical for a person in the 30s living in the democratic West. Of course, there’s a bunch of goals and ambitions that pops up when you start thinking about them, but I’ll put them on the backlog for now.</p><h2>What about you then?</h2><p>There seems to be no lack of people with advice and tips on self-improvement. My main source of inspiration is the <a href="http://5by5.tv/b2w/">Back to Work</a> podcast with <a href="https://danbenjamin.com/">Dan Benjamin</a> and <a href="http://www.merlinmann.com/">Merlin Mann</a> (which is way goofier than the wrapping may seem). The aforementioned Jason Lengstorf also has some resources for you <a href="https://lengstorf.com/2019-personal-retrospective/">in his blog post</a>. Wherever or whoever you get inspiration from, taking the time to reflect and write about where you have been and where you want to go seems like a no-brainer. Whether you want to share it with the world, is entirely up to you.</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/19ad39c7b8913d6debd6031a1917befe67360ea9-5184x3456.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Who are the non-technicals?</title>
      <link>https://knut.fyi/blog/2025-12-20/who-are-the-non-technicals</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/who-are-the-non-technicals</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>We should stop labeling people and ourselves as “non-technical” (unless they really are Luddites).</description>
      <content:encoded><![CDATA[<p>Since I started working in the software industry, I often hear phrasing like this:</p><p>– “I’m non-technical, so this solution doesn’t seem for me.”</p><p>– “We need to communicate to the non-technical people too.”</p><p>My “non-technical” liberal arts education would have set me up to interpret these statements to be about <a href="https://en.wikipedia.org/wiki/Neo-Luddism">Luddites</a>: protesters who denounce technology, worried that we will become slaves to the machines. <br/></p><p>However, it’s rarely people who are prone to break your laptop in protest that’s subject to the category of “non-technical.” More often than not, it’s people that have the same expensive phone and laptop that you have. People who use e-mail, word processors, apps for all sorts of things, spreadsheets, robot vacuums, cars, and whatever back office system they have to endure at their work place. </p><p>Yeah, you probably see where I’m going with this. I don’t think the term “non-technical” is particularly helpful: Not as a self-description, not as a persona, and not as something we use to characterize other people.</p><p>In fact, to label people as “non-technical” is probably reinforcing stereotypes and implicit ideas of power. On the flip side of this label, we are giving a particular agency to the people who make software with code while stealing it away from those who don’t.</p><p>I often observe this self-description used about in a slightly self-deprecating manner: “Yeah, I don’t understand this; I’m non-technical.” If you say this about yourself, why should you be expected to have opinions and critical questions about whatever “technical” go on? In most cases, the other party hasn‘t employed enough care or empathy to jump out of their specialized lingo to communicate what‘s at stake.</p><p>And when we use “non-technical” to decide on our communication strategy, does it help us? I suspect that it most often means that you need to communicate less about the particular features and implementation details of whatever you’re selling and more about the problems that are representative of a wider group of people to whom you’re trying to sell. In probably all cases, you could say “value-based” or “customer-centered.”</p><p>Not understanding much from an article about the particularities of TypeScript’s interface for polymorphic arrays doesn’t make you more “non-technical” than not having read a tome by Tolstoj makes you illiterate. And not (yet) knowing how to program doesn’t make you less technical, as not having crawled over the English channel doesn‘t make you less of a swimmer.</p><p>I’m fairly confident I could get you started with programming in less than a day if you don’t know it already. Because you’re already “technical.” You are so technical that you already know many operational metaphors and models that go into coding. There is nothing in programming you that’s beyond you if you can read this blog post. Sure, it takes time to learn patterns, particularities, and the ins and out of debugging. Sure, some people are more and less motivated by it. </p><p>There is an important difference between assuming that you are intrinsically not able to use or understand tech and being inexperienced with certain technologies.</p><p>So I hope you think twice about what you mean the next time you reach for “non-technical” to denote someone or yourself. Who are you giving power to in the situation?</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/03d320d9e92e0bd69e27529f0908a96a91a28a66-1200x1045.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>The 10 laws of developer experience for content management systems</title>
      <link>https://knut.fyi/blog/2026-04-04/the-10-laws-of-developer-experience-for-content-management-systems</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2026-04-04/the-10-laws-of-developer-experience-for-content-management-systems</guid>
      <pubDate>Sat, 04 Apr 2026 20:52:34 GMT</pubDate>
      <description>These ten laws are a checklist for CMS developer experience in a world where humans and agents both need to build on top of your content system.</description>
      <content:encoded><![CDATA[<p>It&#x27;s never been less hard to build a CMS. A weekend with an AI coding assistant and enough persistence will get you a basic content management system. You can create documents, edit fields, maybe even preview them on a frontend.</p><p>I keep seeing new ones pop up. Not just <a href="https://blog.cloudflare.com/emdash-wordpress/">the recent WordPressesque EmDash launch by Cloudflare</a>; if you go to Reddit’s, you’ll see that every other week someone announces a smol CMS that sits on top of some markdown files aside a web framework or sticks content into the database-service-in-vogue.</p><p>And what I’m seeing in the conversations around these, including <a href="https://ma.tt/2026/04/emdash-feedback/">WP Matt’s… feedback to EmDash</a>, is that most devs are an obsession with where you can host the CMS. While I totally get the concern, worries about lock-in and wanting to deploy something into infra that you like to work with, I think the focus explains why a lot of these CMSes<sup>[1]</sup> don’t scale well. </p><p>I’m not talking WordPress running on a Raspberry Pi not tackling inbound traffic type of scale, but scale in terms of the complexity of your content domain and the teams who are supposed to be productive with it.</p><p>A lot of CMSes, including well-established ones, starts to crackle and crumble when you throw complexity at them. When a second editor joins, when a third team needs the same content in a different format, when an AI agent needs to operate the system on behalf of someone who&#x27;s never seen the admin UI. A CMS that works for one developer editing content alone is a database with a form. A CMS that works for a team, across channels, with agents in the loop, is a different kind of system entirely.</p><p>These things is what I immediately see whenever I give a CMS a spin. And I thought I should write them down as a framework for how you should approach these systems, both in evaluating and making them. I’m starting with the laws for developer experience. There are different laws for editors and for agents. Those are separate posts. (Yes, I&#x27;m committing to writing them. Hold me to it.)</p><h2>A declaration of bias</h2><p>So before diving in I need to address the cloud-sized elephant in the room. I work for Sanity, who has been making a content operating system for the last almost 10 years. You can read these rules as self-serving, because Sanity hits most of these (still plenty of room for improvement though). </p><p>But having a bias doesn’t mean you’re wrong. I’d argue, that my work at Sanity has given me plenty of real-life experience to talk with some authority on these things. And hitting a lot of these points where the point I started using Sanity in the first place, and eventually started to work here.</p><p>I have seen these patterns play out in practice for two decades. I&#x27;ve been shipping WordPress sites since 2005, worked with a wide array of CMSes as a consultant in a lot of different sectors, and over the last eight years at <a href="https://www.sanity.io">Sanity</a>, I&#x27;ve seen implementations ranging from single-developer side projects to Fortune 50 enterprises. </p><p>The laws I keep seeing come into play are the same ones regardless of scale. They just hurt more as the team or content complexity grows.</p><h2>I. Your content model belongs in code</h2><p>Content models defined in code can be version controlled, reviewed in pull requests, tested locally, and generated by AI coding assistants. Content models defined in a UI can&#x27;t.</p><p>This matters more now than it did five years ago. Sure, AI agents can technically click through admin interfaces (<a href="https://www.anthropic.com/news/3-5-models-and-computer-use">computer use is a thing</a>). But it&#x27;s slow, brittle, and burns tokens on pixel-parsing instead of actual modeling work. When your schema lives in code, content modeling becomes a development workflow: describe what you want, the AI writes the schema, you test it locally, you open a PR. When it lives in a database behind a UI, the agent spends its time navigating forms instead of solving your content modeling problem. Same outcome, ten times the cost.</p><p>If you can <code>git diff</code> your content model and an AI assistant can generate a working schema you can test locally before it touches production, you&#x27;re in good shape. If your content model lives somewhere you can&#x27;t version or diff, you&#x27;ve got a problem that only gets worse with scale.</p><h2>II. &quot;Page&quot; is a content type, not an architecture</h2><p>The page-centric content model is <em>so sticky</em>, which is understandable, but it’s the main reason folks get stuck in their CMS down the line. A CMS that organizes content around &quot;pages&quot; and &quot;posts&quot; bakes a website assumption into the data layer. That assumption breaks the moment content needs to go somewhere without a URL. Heck, it even starts to break down when you want to reuse content within a website.</p><p>Content flows to websites, apps, APIs, email, voice interfaces, and destinations that don&#x27;t exist yet. And increasingly, it flows to agent contexts: an AI assistant that needs your product specs to answer a customer question, a translation agent that needs the raw content without layout markup, a personalization agent that needs to assemble content from multiple sources into a single response. None of these consumers think in &quot;pages.&quot; A hotel description isn&#x27;t a &quot;page.&quot; A product with three pricing tiers and regional variants isn&#x27;t a &quot;post.&quot; If your CMS makes these feel like awkward afterthoughts, the architecture is page-centric. You&#x27;ve coupled your content to one presentation context and now every other context requires workarounds.</p><p>Try modeling content that has no URL or route. Products with variants. Configuration objects. If these feel like hacks in your CMS, the architecture is page-centric and you&#x27;ll be fighting it on every project that isn&#x27;t a blog.</p><p>And that takes us to the next law.</p><h2>III. Your content model should be as expressive as your domain</h2><p>If you&#x27;re flattening your business domain to fit the CMS, the CMS is the bottleneck.</p><p>A CMS that ships with text fields, number fields, image fields, and maybe a &quot;JSON string&quot; field covers the basics. But real domains are messier than that. A product with variants, each with its own pricing tiers and availability rules. An event with sessions, each with multiple speakers who are also authors on your blog. A landing page with a flexible layout where marketing can mix hero blocks, testimonial carousels, and pricing tables in any order. These aren&#x27;t edge cases. They&#x27;re &quot;someone&#x27;s just trying to ship on a Tuesday&quot; jobs-to-be-done.</p><p>This goes for rich text too. Rich text stored as HTML couples content to a presentation format. Sure, agents can read HTML. Humans can read assembly language (well… some). That doesn&#x27;t make it the right abstraction. Can you query which documents contain a specific block type? Can you validate that every &quot;callout&quot; block has both a title and a body? Can a translation service process it field by field without accidentally translating your CSS class names? (Yes, that happens.) Structured rich text (typed JSON blocks, like <a href="https://www.portabletext.org/">Portable Text</a>) makes content queryable, &quot;validatable,&quot; and transformable at the field level. HTML makes all of these harder than they need to be.</p><h3>What about Markdown?</h3><p>Here&#x27;s the thing though. There&#x27;s a real tension with how AI agents work today. Agents are great at reading and writing markdown. It&#x27;s their native I/O format. So the tempting conclusion is: just store everything as markdown. But markdown as an I/O format and markdown as a storage format are different things. Agents can use serialization libraries (like <a href="https://npmx.dev/package/@portabletext/markdown">@portabletext/markdown</a>) to convert between structured formats and markdown on the fly. Your storage layer should be optimized for querying, validation, and multi-channel delivery, not for what&#x27;s convenient for one type of consumer. Let the agent translate at the edges.</p><p>When the content model can&#x27;t express the domain, developers build workarounds: JSON blobs in text fields, naming conventions to fake relationships, HTML strings where structured blocks should be. Every workaround is technical debt that compounds. And flat models limit what AI can do with your content. A schema-aware AI that understands field types, relationships, and validation rules can do meaningful work. An AI looking at a bag of string fields can only guess.</p><p>Try modeling a product with variants, each with pricing tiers, or a page with a flexible block layout. Then try querying &quot;all documents where the body contains a code block.&quot; If you&#x27;re reaching for workarounds at any of these steps, the content model is too shallow for the domain it&#x27;s supposed to serve.</p><h2>IV. If you can&#x27;t do it with the API, it&#x27;s not really a feature</h2><p>An API is only as good as what you can do with it programmatically. If core operations (create, validate, publish, set references, bulk update) require a browser session, your API is incomplete.</p><p>This was tolerable when only humans used the CMS. It&#x27;s not tolerable when AI agents, <a href="https://modelcontextprotocol.io/">MCP</a> servers, CI/CD pipelines, and automation workflows all need programmatic access. Every feature locked behind a UI is a feature that can&#x27;t be automated, can&#x27;t be tested in CI, and can&#x27;t be operated by an agent.</p><p>I’m often surprised when I see the limitations that headless CMS vendors put on their APIs outside of the content delivery. Rate-limits that makes it practically hard and painfully slow to do anything meaningful in terms of bulk operations, or simply locking functionality to the UI not making the underlying APIs available for their customers.</p><p>You also see this in the &quot;bring your own AI&quot; pattern: a thin wrapper around OpenAI where you bring your own API token, then figure out how to make the AI service work with your content. If the CMS had a complete API, the integration would be straightforward. The wrapper exists because the API doesn&#x27;t.</p><p>Picture an AI agent with no prior knowledge of your CMS trying to create a document, update its content, validate it, and publish it, all programmatically, without a browser. If that workflow hits a wall anywhere, your API has gaps that will show up in every integration you build from here on out.</p><h2>V. You need a query language, not just endpoints</h2><p>REST endpoints for basic CRUD aren&#x27;t enough for content retrieval, and they&#x27;re barely enough for updates. Especially when you move beyond the page-constrained content model. The moment you need to filter documents by type, project specific fields, resolve references, sort by date, or paginate results, you need a query language. And the moment you need to update a nested field three levels deep without replacing the entire document, you need a patch language too.</p><p>Without a query language, every query pattern requires a different endpoint or a different piece of custom code. Sure, an agent can write that code cheaply now. But every bespoke endpoint is a separate thing the next agent (or developer, or integration) has to discover and learn. A query language gives every consumer of your content, whether it&#x27;s a frontend, an agent, or a third-party integration, the same composable interface. &quot;All articles by this author&quot; and &quot;all articles tagged &#x27;architecture&#x27;&quot; are two filters on the same query, not two different code paths.</p><p>A query language also lets agents explore your content without knowing the schema upfront. &quot;What types exist? What fields do they have? Show me the five most recent documents of this type.&quot; REST endpoints only answer questions you&#x27;ve pre-built answers for. A query language answers questions you haven&#x27;t thought of yet.</p><p><a href="https://graphql.org/">GraphQL</a>, <a href="https://www.sanity.io/docs/groq">GROQ</a>—heck, even SQL—,or something else entirely. The specific language matters less than having one.</p><p>You should be able to sit down (or hand it to an agent) and do a single query that filters by type, resolves references<sup>[2]</sup> two levels deep, and projects only the fields needed, without writing backend code or hunting for a custom endpoint. If that&#x27;s not possible, every new consumer of your content starts from scratch.</p><h2>VI. Every system output is agentic developer experience</h2><p>Error messages, CLI output, validation feedback, API responses, log lines. Every piece of text your system produces is a UX interaction. And in the age of agentic development, it&#x27;s also an instruction to the agent working on behalf of your user.</p><p>There&#x27;s a standard for API errors: <a href="https://www.rfc-editor.org/rfc/rfc9457.html">RFC 9457</a> (&quot;Problem Details for HTTP APIs&quot;), around since 2016. A <code>type</code> URI for stable identification, a <code>detail</code> field for this specific occurrence, extension members for context. Most APIs still don&#x27;t implement it.</p><p>But it&#x27;s not just errors. What does your CLI print when a developer runs <code>--help</code>? What does your dev server log on startup? What does your validation return when a required field is missing? Every one of these surfaces is a place where your system can guide both the developer and the agent. An agent hitting <code>400 Bad Request</code> with no body will retry, hallucinate a fix, or give up. An agent hitting a structured error with available options and a documentation link will self-correct. Same failure, wildly different outcome. I wrote about this in more detail in <a href="https://knut.fyi/blog/2026-04-01/agentic-developer-experience-starts-with-your-system">my post on agentic developer experience</a>.</p><p>Intentionally break something and see what happens. Does the output tell you how to fix it, and does it give enough context for an AI assistant to help debug? Then check your <code>--help</code> text and your startup logs while you&#x27;re at it, because those are just as much a part of the developer experience as the error messages.</p><h2>VII. Your content needs a history</h2><p>Who changed what, when, and why. Rollback. Diff between versions. Audit trails. These aren&#x27;t premium features. They&#x27;re the foundation of trust between your CMS and everyone who uses it.</p><p>Content versioning matters for everyone because it&#x27;s the safety net for every other operation. Schema migration went wrong? Roll back the content. Bulk update hit the wrong documents? Restore from history. Agent made a bad edit that made it to prod? See exactly what it changed and undo it. Without versioning, every destructive operation is permanent, and every automation is a risk.</p><h3>Git ain’t it</h3><p>&quot;But I have markdown files in git.&quot; Sure, and git gives you file-level history. But content versioning is a different problem. Rolling back one field on one document while everything else stays live. Seeing which agent changed which field at 3pm. Branching content for a scheduled release without affecting what&#x27;s published now. These are content operations, not file operations. And in practice, agents struggle to keep content consistent across scattered markdown files in ways that structured, schema-aware storage simply doesn&#x27;t allow.</p><p>This is where vibe-coded CMSes show their broken seams the fastest. Versioning is boring infrastructure work. It doesn&#x27;t show up in the demo. But the first time someone accidentally publishes a draft, or an agent overwrites a field it shouldn&#x27;t have touched, or a migration corrupts 200 documents, you&#x27;ll wish you&#x27;d built it first.</p><p>Make an edit and see if you can tell what changed, who changed it, and whether you can roll back to the previous version without losing anything else. Then try doing that programmatically, not just through the UI. If you can&#x27;t, your versioning is a feature checkbox, not an actual safety net.</p><h2>VIII. Content operations need to work at scale</h2><p>Retagging 5,000 articles. Translating 200 documents. Migrating between schema versions. Backfilling a new required field across your entire content library. These are the operations that reveal the truth about your architecture.</p><p>If single-document operations work but thousand-document operations hit timeouts, rate limits, or data corruption, the system was designed for manual editing. That was fine when content operations were a human clicking through documents one at a time. It&#x27;s not fine when an AI agent is processing your entire library.</p><p>AI makes bulk operations the default workflow, not the exception. A translation agent processing every document in your CMS. A metadata agent backfilling tags based on content analysis. A migration agent restructuring schemas across thousands of documents. These need to work as reliably as single-document edits, and they need to work while humans keep editing. If your bulk operations lock the system or require downtime, you&#x27;ve built a bottleneck into your infrastructure.</p><p>Try updating a single field across 1,000 documents and see what happens. If it works, check whether editors can keep working while the operation runs. If it times out, or if you have to take the system offline to do it, the architecture was designed for one person clicking through documents, not for the workflows that are quickly becoming the norm.</p><h2>IX. Type safety from content model to component</h2><p>Your CMS should generate types from the content model. The developer&#x27;s IDE should know that a &quot;blogPost&quot; has a &quot;title&quot; (string), an &quot;author&quot; (reference to &quot;person&quot;), and a &quot;body&quot; (portable text). Not <code>any</code>. Not <code>Record&lt;string, unknown&gt;</code>. Actual types.</p><p>This applies to query results too, not just document shapes. When you write a query that projects three fields and resolves a reference, the return type should reflect exactly that: those three fields, with the reference resolved to its actual type. If your query language and your type system don&#x27;t talk to each other, you&#x27;re back to casting everything to <code>any</code> and hoping for the best.</p><p>Without codegen, every content access is a runtime gamble. A field gets renamed in the CMS, and the frontend breaks silently. A required field becomes optional, and the component crashes on null. These bugs are invisible until production and tedious to debug. (Ask me how I know.)</p><p><a href="https://www.sanity.io/docs/apis-and-sdks/sanity-typegen">Type generation</a> from the content model closes the loop: the schema is the source of truth, the types are derived, and the compiler catches mismatches before they ship. This is table stakes for any typed language ecosystem. Your CMS should participate in it.</p><p>Try renaming a field in your content model and see if your IDE lights up with errors in the components that reference it. Then write a query that projects three fields and check whether the result type reflects exactly those three fields, before you deploy. If the answer to either is no, you&#x27;re flying blind between your content layer and your frontend.</p><h2>X. Your content should be more portable than your platform</h2><p>Your content will hopefully outlive your current tech stack. It will outlive your current framework, your current hosting provider, and probably your current CMS. The question is whether it can leave cleanly.</p><p>I can’t fathom (well, maybe a little bit) how much money has been billed on just the moving content from one system to another throughout the years. Very often, it was way less hard to just scrape the website, than get it out of the underlying DB. </p><p>Yes, this is one of the things that is now more approachable with AI, but storing your content in presentation formats like HTML is still a waste of time and tokens, and it makes the implementation in modern web frameworks weird and limited.</p><h3>Po(r)table content in proprietary pots</h3><p>This is about content portability, not platform portability. A hosted CMS can be perfectly fine. The question is whether your content is locked into a format, a schema representation, or a query pattern that only works with that one system. Can you export your content as standard JSON? Can you access it through standard protocols? Can you switch frontends without migrating content? Can you add a mobile app without duplicating your content layer?</p><p>The flip side matters too: can you bring content in? If migrating to your CMS requires a six-month data transformation project, the lock-in works both ways. Good content portability means standard formats in and out, APIs that don&#x27;t require proprietary clients, and content that&#x27;s structured enough to be useful outside the system that created it.</p><p>Export all your content and see if another system can read it without a custom parser. Try switching your frontend framework without touching the content layer. If a new integration protocol emerges next year, you want adding support to be a weekend project, not a six-month rebuild.</p><h2>Old laws for new times</h2><p>These laws aren&#x27;t new. Most of them have been understood by the CMS community for a decade or more. What&#x27;s new is that vibe coding makes it easy to build something that violates all ten in a weekend and looks great doing it. The hard problems don&#x27;t show up in the demo. They show up when a second editor joins, when content needs to flow to a channel you didn&#x27;t plan for, when an AI agent tries to operate the system programmatically, or when you need to migrate and discover your content is welded to your infrastructure.</p><p>Build a CMS if you want to. The tools have never been better for it. But if you want other people to use it, these are the problems you&#x27;ll need to solve. And if you&#x27;re evaluating one, these are the questions to ask. Feel free to respond with your own list.</p><hr /><ol><li id="fn-a3d049def0ac"><p>I can never decide if I like “CMSs” or “CMSes” more. </p></li><li id="fn-dc0bda898c48"><p>A “reference” here is some kind of indexed and queryable link between two separate pieces of content.</p></li></ol>]]></content:encoded>
      <author>Knut Melvær</author>
    </item>
    <item>
      <title>Making tech survivable: What can men do?</title>
      <link>https://knut.fyi/blog/2026-03-16/making-tech-survivable-what-can-men-do</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2026-03-16/making-tech-survivable-what-can-men-do</guid>
      <pubDate>Mon, 16 Mar 2026 16:32:14 GMT</pubDate>
      <description>23 tips for those who want to contribute to a better workplace for everyone who wants to be in tech.</description>
      <content:encoded><![CDATA[<p>I recently read Patricia Aas’ heartbreaking, but pertinent blog post “<a href="https://patricia.no/2018/09/06/survival_tips_for_women_in_tech.html">Survival Tips for Women in Tech</a>.” Go read it, do share it, and come back afterward. It resonates well with what I’ve learned from talking to other women in tech and their experiences, and my own observations of how these unfair structures allow for exclusions of people who are not white men. There’s a lot of implicit advice for us in Patricia’s blog post, but I wanted to complement it because I hope it can be useful if “one of the guys” joins this conversation.</p><p>I also sincerely believe that the field is changing and that there’s a lot of us that has to re-evaluate our perspectives if we want to be taken seriously at one of the future’s attractive workplaces. So this is my attempt at a guide for men in tech. Know that I’m writing this knowing that there are things I may have not considered or have enough knowledge about. And consider it a fork of Patricia’s post, and an open source project where pull requests and code reviews are welcome from all who care to. I know that this text smells of a narrow heterogeneous gender dichotomy (men vs. women). Even though these tips are directed at a stereotypical sense of men — whatever your definition is, they are obviously not restricted to them.</p><h3>1. A team’s success doesn’t rely on being a fraternity, it relies on different people’s ability to work together</h3><p>It’s time to retire the idea that success in your team requires that all of its members are “in on the jokes,” like the same type of past time activities, or should tolerate shitty behavior because it’s supposed to be a sign of sibling-like love. Don’t take women’s adversity to join in on certain activities to mean that they “aren’t any fun” or that “that they look down on you.” People always have their reasons, there’s no harm in questioning if how you act at work can be unpleasant for others.</p><h3>2. Give credit to and honor other’s work</h3><p>For all its flaws, the academy has one system going for it: the practice of citation. It’s generally anathema to steal ideas or research of others without giving it credit (not saying that it doesn’t happen). In tech, we are entirely dependent on each other’s capacity to great work, because despite what some may opine, you can’t write all the code yourself. And I’ll let you in on a secret: It feels way more powerful to be part of making sure that another person’s good idea comes into fruition. Chances are that this person has more good ideas on the way, and you may have the privilege to work with that person.</p><p>We know that women are often talked over in meetings. We know that ideas are easily stolen by the most eager and outspoken in these settings. You can be the one that reminds the group who was its originator and place that person back in the narrative. Be conscious about this in your written communication also.</p><h3>3. Don’t think that it’s other people’s job to ensure that the workplace is safe</h3><p>If the <a href="https://patricia.no/2018/09/06/survival_tips_for_women_in_tech.html#3-hr-is-not-your-friend">advice for women is that they can’t trust Human Resources</a>, not even if they’re women and apparently say the right things, the responsibility falls on you to set expectations from leadership. It’s perfectly OK for you as a man to request the strategy for making sure that people are given equal treatment in the workplace. You can require that there are safe ways to report abuse. Often this role falls to women, and they are immediately categorized as “troublemakers” or “pissy.” Relieve those women of that role, I doubt you’ll risk any consequences, and if you do, it lends only legitimacy to what you did.</p><h3>4. Women in tech are first and foremost technologists</h3><p>We have different roles and different times in our lives. We are sons, fathers, brothers, employees, coders, front-enders, project managers, strangers, friends, colleagues, but very seldom we are reduced to “the man” in the workplace. Women in tech are often reduced to, well, women in tech. Trust me, most of them don’t want to be. They want to do interesting work and be recognized for it. Sounds familiar? We all play an array of different roles, sometimes gendered or not, but I don’t know any male programmers that want to be celebrated for writing manly code. Don’t go looking after “a woman programmer”, but make sure that you have been in contact with and considered people of all genders.</p><h3>5. Don’t use alcohol as an excuse for bad behaviour</h3><p>I’m ambivalent about alcohol at the workplace. It can obviously be a fun social lubricate, and wine, beer, and drinks can be a joy for the senses. But too much of it, or for some, any of it, are often destructive, and drunkenness is used as an excuse for douchebag behavior. You should be aware if you’re able to behave yourself when you drink. If you’re not 100% sure that you can behave when you drink, get feedback from someone, ideally a woman. I doubt it’s feasible to quit alcohol on functions altogether, but make sure that you request that there are good alcoholic-free drinks as well, and make alcohol a complementary thing, not the main activity of the evening.</p><h3>6. You’re a nice guy, but you’re probably part of the problem</h3><p>I’m guilty of this. Just because I care about gender equality, it doesn’t mean that I’m not failing to work against it or take advantage of my privileged position at the cost of others. Being circumspect and self-conscious of this doesn’t mean that you have to be self-flagellating or feel like a bad person, it just means that you’ve to put a little work into being a decent person to others. You’re no use if you don’t contribute by calling out abusive behaviour, or don’t make sure that people are given a fair chance to succeed and evolve.</p><h3>7. Give women space</h3><p>Both literally and figuratively. Be mindful that people who are constantly reminded of being of and in the minority needs space to be themselves, as coders, nerds, or whatever. Your input or presence isn’t always required. Statistics show that many of the women you will work with have experienced physical harassment in some way. People respond differently to these experiences, but it doesn’t hurt to ask if one would like the door open or closed if you have a one-to-one meeting, or let them choose the location.</p><h3>8. If you’re given a cake, take that as good feedback</h3><p>Patricia’s tips are to bake a cake for great people. This cake is <em>not a lie</em>. If you succeed in being a good person for your colleagues, chances are that you’ll get rewarded for it. If you are like me, this can feel a bit unpleasant, because chances are that you behaved like a decent person, which shouldn’t qualify for special treatment. But eat the cake and appreciate the gratitude. And remember, you can bake cakes too.</p><h3>9. Eat lunch with women in tech (but not as a date!)</h3><p>If you follow these tips, you may get the chance to talk and learn about other people’s experiences in the field. Much of what I have learned, has been over a lunch, where I have tried to listen. You may feel the urge to argue or get defensive. Save that for later if you must. You’ll be surprised that even female colleagues that you recognize as “strong” and “outspoken” have bad experiences.</p><h3>10. Help out documenting women’s work</h3><p>Patricia advice women to make slides and document their work to not lose credit. That sounds like a lot of work. The least of what you can do is to volunteer as meeting secretary and make sure that the work is documented and credited where credit is due. This is significant at a higher level: It’s important that we make sure that women take part in a team’s success story. The book <a href="https://www.amazon.com/Broad-Band-Untold-Story-Internet/dp/0735211752"><em>Broad Band: The Untold Story</em></a> (which is a great book on its own right) by <a href="https://twitter.com/theuniverse">Claire L. Evans</a> shows how many essential and important inventions and practices has been made by women in the history of digital technology, only to be forgotten when men write its history (looking at you <a href="https://en.wikipedia.org/wiki/Hackers:_Heroes_of_the_Computer_Revolution">Steven Levy</a>).</p><h3>11. Save your criticism of later</h3><p>It’s so easy to shoot down ideas. It’s harder to come up with them. Sure, you have insights, experience, and valid points, but more often than not, it’s better to let people explore and make their ideas more concrete. It will also give you a chance to chew it over, and if they make a proof of concept, you’ll both have a more informed and fruitful discussion.</p><h3>12. Question team-building activities, especially where women have to dress differently</h3><p>Hell, I don’t feel very comfortable bathing with anyone. I don’t get the appeal of strip clubs. I don’t see how themed parties are very useful in building good teams. Sure, people may find all these things great fun, and that’s ok. But they should be voluntary and complimentary. If the work isn’t team building of itself, you should probably find some more interesting problems to solve.</p><h3>13. Be wary if women quit your workplace</h3><p>People quit for boring and legitimate reasons. But sometimes they leave because they can’t bear it anymore. I have many times been caught surprised when I have after the fact learned that some past colleague has gone through some shit at the workplace. Many of these things never hit the surface.You cannot assume that people will automatically recognise you as an ally (you probably look exactly the same as the people who are the reason your colleague is leaving). Again, it’s a good time to ask leadership about how we ensure a safe workplace.</p><h3>14. Your criticism is least useful in the form of public barrage</h3><p>I’ve been a part of groups where harsh feedback has been given openly, but I have always found this behaviour in myself and others a bit weird: Some men tend to be obtuse when giving critical feedback. We rationalize it as “being honest” and “not bothering dressing it up nicely,” but honestly, it makes us feel as shitty both as the giver and receiver. So there’s no reason to jump on the dogpile. Good, useful criticism comes with empathy. If you want your feedback to have lasting effects, it has to come from an understanding of the position the work comes from. Also, there are just so many hills you can die on. Give people a chance to learn from own mistakes.</p><h3>15. Be the one chosen for code review</h3><p><a href="https://www.youtube.com/watch?v=Eb_sbkcDa_8">Patricia have great advice for doing code reviews</a>. It’s mostly about how to do good programming work. Some men have the impression that women generally are not as good at programming, and use code reviews to remind everyone of that. <a href="https://www.theguardian.com/technology/2016/feb/12/women-considered-better-coders-hide-gender-github">They are wrong</a>. We all make stupid programming mistakes. We’re all learning the craft, and that process never ends. People solve things differently, and that can be a learning opportunity. And seriously, if you find yourself criticising style choices, your team either need to choose a linting standard, or you shouldn’t do the review in the first place.</p><h3>16. Be generous with your knowledge. Teach those who want to learn from you</h3><p>If you can be part of making someone a better coder than you, that’s a privilege you want to be a part of. Teaching what you know to others, is also an excellent way to improve your own skills. Remember that teaching isn’t the same as telling or explaining. Good teaching comes from learning what your student needs to increase her expertise or knowledge. It requires a lot of listening.</p><h3>17. Demand diversity</h3><p>Make sure that your job ads are read and vetted by different people, including women. Remind recruitment that women often have to be contacted directly. Ask conference hosts what they’ve done to ensure diversity on the stage because then you are taking a place that could have been held by a woman. Be open that a new colleague’s role isn’t to be your new buddy, it’s to set your team up for being able to solve problems. And don’t just demand it, offer to help in any way you can.</p><h3>18. Don’t tolerate locker room talk</h3><p>I don’t think there’s much more to say. It’s dumb and unnecessary, and you should call it out.</p><h3>19. Be a powerful ally</h3><p>If you gain someone’s trust because you manage to behave like a decent human being, use that trust and your privilege to support that person be heard in meetings and workplace processes. Be prepared and willing to let others shine.</p><h3>20. You probably don’t need to put a sexy lady on your slides</h3><p>If your work isn’t sexy enough of itself, it won’t certainly help to augment it with something that makes some people uncomfortable. You may find this prudish, but that’s probably because you haven’t experienced being reduced to a body to any extent. Being an ally also means calling out colleagues and peers who still think this sort of behaviour is fine.</p><h3>21. Don’t assign non-technical work to women that you don’t also do yourself</h3><p>If a woman is hired as a programmer, coding is probably what she should be working with. If you find yourself in need of a secretary, project manager or user researcher, you should probably hire one. If you have a small team, which require people to cover more roles, be circumspect of which roles you give whom. Just because you recognize that women may be “good with people,” it doesn’t mean she should be doing all the user research. Perhaps the most talkative person in the meeting should take the role as a notetaker and be forced to listen.</p><h3>22. Educate yourself</h3><p>Read books like <a href="https://www.amazon.com/Brotopia-Breaking-Boys-Silicon-Valley/dp/0735213534">Brotopia</a>, and <a href="https://www.amazon.com/Broad-Band-Untold-Story-Internet/dp/0735211752">Broad Band</a>. Follow <a href="http://twitter.com/vuevixens">women programmers on twitter</a>. Read blogs. Listen to <a href="https://www.relay.fm/rocket">podcasts</a>. Watch conference talks that deal with social issues in tech. Learn something about <a href="https://en.wikipedia.org/wiki/Discourse_analysis">discourse analysis</a> and <a href="https://en.wikipedia.org/wiki/Literary_criticism">literary criticism</a>. Think about how words and language tie in with power. Be open that knowledge can be acquired in different ways, and be skeptical to people who claim that they possess a scientific objective truth about how gender is supposed to work. If they claim scientific superiority they should be able to host a colloquium where you review and discuss published research. Take the time and read Daniel Kahneman’s <a href="https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555">Thinking Fast and Slow</a> and reflect on all the cognitive biases that are in effect every day, for us all. There’s some good meeting advice in there as well.</p><h3>23. Think about the fact that Survival Tips for Women in Tech has to be written in 2018, in Norway</h3><p><a href="https://www.fafo.no/index.php/zoo-publikasjoner/fafo-rapporter/item/seksuell-trakassering-i-arbeidslivet-3">Official reports</a> from 2017 have shown that 1 in 12 women have experienced sexual abuse at the workplace in Norway (so that’s not counting what may have happened on school, or at home). Think about it. Norway is one the world’s top countries concerning gender equality. Still, chances are that one of twelve women that walks through the door at your workplace, has experienced some sexual transgression in some way, and almost certainly, have had shitty experiences that comes from her merely being a women and in the minority.</p><p>There are situations that may seem completely harmless for you, that may for people that have experienced abuse, feel is unsafe and unpleasant. It’s impossible to account for this at every turn, but be at least open and understanding if someone hints to that may be the case. This affects their ability to do good work. This affects people’s opportunity to be… people.</p><p>I know I risk very little writing this, even though I imagine some of my fellow male technologists may call me out as a social justice warrior, or roll their eyes for me being politically correct. Well, I hope they get over themselves. I will still be recognized for the other work I do, and not just “an angry feminist.” I may also be accused of virtue signaling. Be that as it may, I’m not saying I have succeeded in following these tips myself. But I think it’s vital that people like me, also join the conversation. For ultimately, it’s about figuring out how we all can make room for everyone to go explore in this short time we spend in existence, in a way we find fulfilling. And that’s a hill I want to die on.</p><p>(<em>Thanks to Patricia for</em> <a href="https://patricia.no/2018/09/06/survival_tips_for_women_in_tech.html"><em>writing the article</em></a> <em>that inspired this one, and for her feedback on this text. And thanks to</em> <a href="https://medium.com/@finiteattention"><em>Chris Atherton</em></a> <em>for her helpful comments.</em>)</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/9c50a1fe21ef0a843927430e5c208f65bdfe2840-1000x420.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Ode to LICEcap, the simple GIF screen capture tool</title>
      <link>https://knut.fyi/blog/2025-12-20/ode-to-licecap</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/ode-to-licecap</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>Why my go-to tool for screen capturing to animated GIFs is LICEcap.</description>
      <content:encoded><![CDATA[<p>The folks at Apple Computers Inc. really wanted me to upgrade to <a href="https://arstechnica.com/gadgets/2019/10/macos-10-15-catalina-the-ars-technica-review/">macOS Catalina</a>, so I did. After having nervously lingered in front of the unmoving progress bar of impending doom for two hours while the upgrade script made sure that all of my millions of node_modules files had the correct sandboxed permissions, I was finally let into my tool of digital self-expression again. Only to discover that the brilliant engineers of Palo Alto had decided that 32-bit apps weren’t in vogue anymore. You know, like reliable keyboards and SD-card slots.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/ec262548b665f086808ace3e4fb02a0bb543d3f4-675x554.png?w=800&fit=max&auto=format" alt="The 32-bit version of LICEcap can't be opened on macOS Catalina" /><figcaption>Not being able to open my beloved LICEcap, prompted me to think of how much I actually use this app.</figcaption></figure><p>But this post isn’t about how Apple has lost its edge with its <a href="https://goo.gl/maps/QsTYwoDtEozHpMgr7">new perfect circular new campus</a>, but about one of my favorite little apps, the GIF screen capture tool <a href="https://www.cockos.com/licecap/">LICEcap</a>.</p><p>Not a day goes by without me having to record something I do on the screen. It can be to document some bug I found, show my colleagues at <a href="https://www.sanity.io">Sanity.io</a> something cool, a clever tweet, or useful visual context when I’m helping other people out some project. I know you should think twice before putting animated GIFs on your webpage (because they tend to be huge), but I sometimes make that sin too.</p><p>There are loads of cool screen capture to GIF tools out there. An obvious mention is <a href="https://getkap.co">Kap</a> by <a href="https://github.com/wulkano/kap/graphs/contributors">these wonderful people</a>. It does a lot more than LICEcap, and is even written in the world-eating JavaScript programming language. In fact, I used it to screen capture how LICEcap works.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/ffbaa72d85ee1494b1d7b885f3275e97ac46d1b2-1008x942.gif?w=800&fit=max&auto=format" alt="Animated GIF showing how to record with LICEcap" /><figcaption>Using LICEcap to record some interesting free coding.</figcaption></figure><p>But the reason I’m always reaching for LICEcap when I need those moving pixels captured is that it records directly to the file. No post-processing. Apparently, that is a killer feature. Being able to quickly do the screen capture and have the GIF ready for sharing in the instant you stop the recording is really convenient. Turns out, convenience trumps feature lists.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/5093216653dcfa256cf1a302b53d3a64bcd82dfa-841x463.png?w=800&fit=max&auto=format" alt="The few recording options for LICEcap. " /><figcaption>Not a lot of bells and whistles, but enough in most cases.</figcaption></figure><p>LICEcap doesn’t have a bunch of options and settings. But that’s fine for most parts. If I need more, that usually means that I should do the recording with something like <a href="http://www.telestream.net/screenflow/overview.htm">ScreenFlow</a> instead.</p><p>I actually thought my days with LICEcap were over, but writing this post made me discover that a 64-bit version has been out since February 2018, so shame on me.</p><p><em>Doing some minimal amounts of research for this post, I also discovered that LICEcap’s developer is <a href="https://en.wikipedia.org/wiki/Justin_Frankel">Justin Frankel of Winamp fame</a>.</em></p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/03f5f0d8dd5aed58ae950ac45e76826a746be2e2-1552x933.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Smash your writer’s block with The Hulk Summary™</title>
      <link>https://knut.fyi/blog/2025-12-20/smash-your-writer-s-block-with-the-hulk-summary</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/smash-your-writer-s-block-with-the-hulk-summary</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>WRITING IS HARD

YOU THINK TOO MUCH!

THE SOLUTION

THE HULK SUMMARY!</description>
      <content:encoded><![CDATA[<ul><li>WRITING IS HARD</li><li>YOU THINK TOO MUCH!</li><li>WE HAVE A SOLUTION</li><li>THE HULK SUMMARY!</li><li>CHANNEL INNER HULK VOICE!</li><li>BULLET LIST!</li><li>FEW WORDS</li><li>SAY IT. EFFECTIVE!</li><li>NOW, SMASH CAR!</li></ul><p>Look, when you work with content, it’s not always easy to get going. Perhaps you only manage to come up with the same tired clichés, or you’re too close to the product and find yourself knee-deep on a tangent describing how the design choices were inspired by something your uncle said in a birthday party.</p><p>Whatever it is that keeps you from writing that succinct post that presents the what, how, and why to your readers without wasting their time, there is a remedy. We called it “The Hulk Summary”.</p><p>The method is simple. First channel your inner Hulk voice (everyone has one hidden in them somewhere), add a touch of rage and use it to describe whatever you need to get writing on. Hammer down that shift key (caps lock is a cop-out) and type max 4 words for each bullet to capture the core sentiment of what you’re arguing. The constraints will set your mind free.</p><p>You might not find crude shouting methodology enough to tease out the core of what you try to say. I&#x27;m happy to announce that The Hulk Summary™ is fully compatible with renowned communication frameworks proven by probably too expensive consultancy hours, such as the <a href="https://strategyu.co/scqa-a-framework-for-defining-problems-hypotheses/">SCQA-model</a> from <a href="http://www.barbaraminto.com/">The Minto Pyramid Principle</a>. Put differently:</p><ul><li>WHAT SITUATION!</li><li>COMPLICATION! DIFFICULTIES!</li><li>I HAVE QUESTION!</li><li>ANSWERS ARE DUE.</li></ul><p>This method will most definitively not be developed further. We have loads to things to ship over at <a href="https://www.sanity.io/blog">sanity.io</a>. But now that it is introduced to a wider audience, we would love to hear your success stories about how it saved your whole content marketing department from writer’s block and pencil fear. Or how you made an interesting friendship from using it as a quirky icebreaker at a gathering. Literally hit us up on <a href="https://twitter.com/kmelve">Twitter</a> and let us know, hashtag thehulkmethod.</p><p><em></em></p><p><em>(No Banners were hurt in writing this blog post.)</em><br/></p>]]></content:encoded>
      <author>Knut Melvær, Even Westvang</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/7b1b7cc7a4cf094b24cfd1ea6a6fa224865bc9e0-1200x800.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Agentic developer experience starts with your system, not your prompts</title>
      <link>https://knut.fyi/blog/2026-04-01/agentic-developer-experience-starts-with-your-system</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2026-04-01/agentic-developer-experience-starts-with-your-system</guid>
      <pubDate>Wed, 01 Apr 2026 18:21:47 GMT</pubDate>
      <description>The new &apos;time to hello world&apos; isn&apos;t determined by a developer reading your getting started guide. It&apos;s someone typing a naive prompt into an agent. How should we think about that?</description>
      <content:encoded><![CDATA[<p>In 1998, I right-clicked on websites I thought were cool and hit “view source.” The HTML was right there. The thing I was looking at and the thing that made it were the same language. I could copy it, change it, reload. That’s how I started coding. Shamelessly copying other people’s work with a lot of persistence.</p><p>There was the “[insert technology] for Dummies” series, computer magazines with tutorials, and the occasional workshop at the local Internet café. The path from curiosity to running code was: find an example, read it, change it, see what happens.</p><p>Fast forward almost three decades (yikes!) to a few weeks ago.</p><p>I’m standing by a whiteboard at Sanity HQ in San Francisco, talking to the team building our SDKs about what happens when agents try to use our stuff. They wanted to know: how do we write better skills files? How do we give devs better prompts?</p><p>These are intuitive questions. All of us have spent the last couple of years figuring out what to tell agents to make them useful. Still, I told them they were starting in the wrong place. I’ve been working through this with several of our teams, and what I keep finding is that the answers aren’t where you’d expect.</p><p>When someone types “build me an app with [your product]” into Claude Code, unlike a Google search, it just does stuff. It picks packages. It picks patterns. It makes architectural decisions based on statistical opinions trained on everything it’s seen. And it does all of this before your user has read a single line of your documentation.</p><h2>The new “time to hello world”</h2><p>If you make developer tools, you probably think about “time to hello world.” How fast can a developer go from zero to running code with your product? You optimize your getting-started page, your quickstart guides, your API key flow. You design for a human reading your docs. You make it easy to copy-paste. You make wizards that scaffold projects from choices.</p><p>But now there’s another type of cognition operating between your product and the developer. Language models wrapped in different affordances (an IDE, web or terminal apps, different system prompts, MCPs, skills), what’s often called “agent harnesses.” These harnesses sometimes go fetch your docs, sometimes use your MCP tools, sometimes just predict code from training data. The line between “what the LLM knows” and “what the harness finds” is blurry, and it’s getting blurrier.</p><p>What’s consistent is this: someone types a naive prompt. “Build me a content-driven app with Sanity.” “Set up authentication with Clerk.” “Deploy this to Netlify.” There might be no docs consulted. No getting-started page visited. Just a prompt and an expectation.</p><p>That naive prompt is your new first impression.</p><h2>The new kind of developers</h2><p>The person typing that prompt isn’t always who you’d expect. Agentic coding tools have lowered the barrier enough that people who wouldn’t have called themselves developers two years ago are now building applications. Designers prototyping interfaces. Marketers building bespoke dashboards. Founders shipping MVPs. They’re not going to read your getting-started tutorial. They might not even know they should. (We had these users before too. They just used to hit a wall that forced them to learn or hire someone. The agent removed the wall but not the gap in knowledge behind it.)</p><p>We’re seeing this at Sanity too. Builders on Reddit who report that “<a href="https://www.sanity.io/blog/2025-12-20/who-are-the-non-technicals">they are not technical</a>” while touting that they went to production with a website using Next.js and Sanity. That tends to work pretty well, because most models have seen a lot of Next.js plus Sanity code.</p><p>But when the task goes beyond what’s well-represented in training data, things get interesting. I work at <a href="https://www.sanity.io">Sanity</a>, have done for nearly eight years, so I’m obviously invested in how this plays out for us. An agent asked to build a Sanity application, without specific guidance, grabbed the Sanity client, grabbed Next.js, and tried to build a generic app. It had no idea what our <a href="https://www.sanity.io/docs/app-sdk">App SDK</a> was. It defaulted to whatever had the most representation in its training data. I started calling this “popularity horror”: the agent follows the most-trodden path, not the correct one.</p><h2>Where you actually have control</h2><p>This is where the current conversation about “Agent Experience” gets interesting.</p><p>Matt Biilmann <a href="https://biilmann.blog/articles/introducing-ax/">coined the term AX</a> in early 2025 and did something important: he gave people a name for a design problem nobody was talking about systematically. In his <a href="https://biilmann.blog/articles/one-year-of-ax/">one-year update</a>, he lays out four pillars: Access, Context, Tools, Orchestration. It’s a useful map of the territory. His point about context engineering, managing what’s in the agent’s context window during its tool-use loop, is especially sharp. And the <a href="https://biilmann.blog/articles/ax-in-practice/">“deploy first, claim later” pattern</a> that Netlify, Clerk, and Prisma have adopted is a genuine innovation in onboarding design.</p><p>What I want to add is a question about sequencing. When you have a team with limited engineering time, where do you start?</p><p>All four pillars are worth investing in. But I keep finding that the system layer (your API shape, your error messages, your SDK abstractions) does double duty: it improves the experience for agents AND for the humans who were already using your product. The instruction layer (skills, <a href="https://llmstxt.org/">llms.txt</a>, <a href="https://modelcontextprotocol.io/">MCP</a> descriptions) is important work, but it’s agent-specific. If I had to pick where to spend the first sprint, I’d pick the system.</p><p>Here’s how I think about where your design control actually sits:</p><p><strong>Low control: the user’s prompt.</strong> You can’t influence what someone types into Claude Code. Zero-control territory.</p><p><strong>Medium control: the instruction layer.</strong> Skills files, <a href="https://llmstxt.org/">llms.txt</a>, <a href="https://modelcontextprotocol.io/">MCP</a> tool descriptions, documentation. You write these, and they matter. But they get interpreted, compressed, sometimes dropped. Whether the agent finds your docs through its harness or predicts from training data, the instruction layer is always mediated. Cloudflare’s developer docs produce an llms-full.txt that’s <a href="https://getpublii.com/blog/llms-txt-complete-guide.html">3.7 million tokens long</a>. That’s not fitting in any context window.</p><p><strong>High control: your system’s surface.</strong> API responses, CLI output, error messages, SDK abstractions. These are what the agent actually touches when it interacts with your system, whether it’s predicting code or calling your tools. And this is where I keep finding the real leverage.</p><h2>Error messages as instructions</h2><p>There’s already a standard for this, and it’s been around since 2016. <a href="https://www.rfc-editor.org/rfc/rfc9457.html">RFC 9457</a> (“Problem Details for HTTP APIs”) defines a JSON format for error responses with structured fields: <code>type</code> (a stable URI identifying the problem), <code>title</code> (human-readable summary), <code>detail</code> (what went wrong this time), and extension members for anything else. The content type is <code>application/problem+json</code>.</p><p>The difference between a useless error and a useful one is the difference between the agent spinning its wheels and self-correcting:</p><pre><code class="language-json">// What the agent gets from a lazy error:
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid request"
}</code></pre><pre><code class="language-json">// RFC 9457 Problem Details: the agent can work with this
{
  "type": "https://api.sanity.io/problems/document-type-not-found",
  "title": "Document type not found",
  "status": 400,
  "detail": "Document type 'post' not found in dataset 'production'",
  "availableTypes": ["article", "page", "author"],
  "hint": "Did you mean 'article'? See https://sanity.io/docs/schema-types"
}</code></pre><p>Same HTTP status code. Wildly different outcome. The <code>type</code> URI gives the agent a stable identifier it can pattern-match on across occurrences. The <code>detail</code> field explains this specific failure. The extension members (<code>availableTypes</code>, <code>hint</code>) give it everything it needs to self-correct. This isn’t a new idea. It’s a standard that most APIs still don’t implement.</p><p>I ran into a version of this with our own tooling recently. I was trying to deploy Sanity functions from CI using our Blueprints CLI and hit <code>Missing scope configuration for Blueprint</code>. The scope config (project ID, stack ID) lives in <code>.sanity/</code>, which is gitignored because it also contains build cache. CI clones fresh, so the config is never there.<br/><br/>It turns out there’s a dedicated <a href="https://www.sanity.io/docs/blueprints/blueprint-action">GitHub Action for Blueprints deployments</a> that handles all of this. But neither I nor my agents found it. The agents went straight to the CLI (the path of least resistance), hit the error, and started trying to work around it. One agent, given enough rope, actually read the CLI source code and discovered that the scope can come from environment variables (<code>SANITY_PROJECT_ID</code>, <code>SANITY_BLUEPRINT_STACK_ID</code>), bypassing the config file entirely. It solved the problem. But it solved it the hard way, by reverse-engineering the internals instead of finding the purpose-built solution.</p><p>The interesting part is what happened when I shared this with the Blueprints team. Within minutes we were sketching out improvements: what if the error message mentioned the GitHub Action? What if <code>blueprints init</code> offered a <code>--with-github-action</code> flag? What if <code>--help</code> had a CI/CD section? The building blocks were all there. The surfaces just didn’t connect them. Something like:</p><pre><code class="language-text">Error: No scope configured for this project.

  To set up locally:
    sanity blueprints init

  To deploy from CI (recommended):
    Use the official GitHub Action:
    https://sanity.io/docs/blueprints/blueprint-action

  Or set environment variables:
    SANITY_PROJECT_ID, SANITY_BLUEPRINT_STACK_ID

  Run 'sanity blueprints deploy --help' for all options.</code></pre><p>That’s the same information the agent eventually found by reading source code, surfaced at the moment it’s needed. The agent hitting this error goes from spinning to self-correcting. The human hitting it goes from frustrated to deployed.</p><h2>The experiment</h2><p>To make the SDK point concrete, I ran a simplified experiment. I opened the <a href="https://console.anthropic.com/">Anthropic Workbench</a>, no IDE, no skills files, no system prompt, just Claude Sonnet and a text box, and typed:</p><blockquote>Build me a custom content approval dashboard for Sanity. Editors should see a list of documents pending review, be able to open and read each document, and approve (publish) or reject them. Use React.</blockquote><p>Some context: the <a href="https://www.sanity.io/docs/app-sdk">App SDK</a> is a React toolkit for building custom content tools on top of Sanity. It provides hooks for listing documents, reading them with real-time updates, and publishing them atomically. If the agent finds the SDK, this is a straightforward build.</p><p>The agent did not find the SDK. (It’s relatively new, so the training data is dominated by older patterns.)</p><p>10,335 tokens of code. Built everything from scratch on top of <code>@sanity/client</code>. To publish a document, it predicted three separate API calls with no transaction and race conditions between steps. The production-grade version, <code>publishDocument(handle)</code>, is a single atomic operation in the SDK the agent didn’t know existed.</p><pre><code class="language-typescript">// Patch the draft
await client
  .patch(`drafts.${documentId}`)
  .set({ approvalStatus: 'approved', reviewedAt: new Date().toISOString() })
  .commit()

// Copy draft to published
const draft = await client.getDocument(`drafts.${documentId}`)
const { _id, ...docWithoutId } = draft
await client.createOrReplace({ ...docWithoutId, _id: documentId })

// Delete the draft
await client.delete(`drafts.${documentId}`)
</code></pre><p>Second run, I added “real-time” to the requirements. Worse, not better. 17,840 tokens (72% more code), 150 lines of hand-rolled state management. Still no SDK.</p><p>Third run, I added a system prompt listing the SDK hooks and their signatures. Dramatically different. The result was dramatically different. The agent used every hook I mentioned. <code>useDocuments</code> for listing. <code>useDocument</code> for reading with real-time updates. <code>publishDocument(handle)</code> for the atomic publish. Suspense boundaries for loading states. A standalone App SDK app with <code>&lt;SanityApp&gt;</code> provider instead of a Studio plugin. Cleaner architecture, fewer tokens (11,832), more correct code.</p><p>But to write that system prompt, I had to already know the answer. The instruction layer worked, but it required the human to have the expertise the agent lacked.</p><h2>The hallway, not the signage</h2><p>There’s an analogy I keep coming back to. If you’re designing a physical space, you can put up signs telling people where to go. Or you can design the hallways so the natural flow takes people where they need to be. Both matter. But if your hallways are confusing, no amount of signage will fix it.</p><p>Skills files and llms.txt are signage. Your API design is the hallway.</p><p>That’s what good developer experience has always been: making your tooling less vulnerable to oversight. Less vulnerable to the human forgetting a step, skipping a doc page, not knowing what they don’t know. Agents just add another layer where oversight can happen. The SDK absorbs that layer the same way it absorbs the human one. The error message that tells you what went wrong and how to fix it works for both.</p><h2>Three questions for your next sprint</h2><p>If you’re on a team building developer tools and you want to start somewhere concrete, here are three questions I’d run every surface through:</p><h3>1. What does the agent see when it fails?</h3><p>Pull up your error messages, your CLI output on bad input, your API responses on invalid requests. Read them as if you have no context. Does the failure tell you what went wrong and how to fix it? Or does it just say “invalid request” and leave you guessing?</p><p>The Blueprints CLI example is mine. Yours will be different. Maybe it’s an API that returns <code>400 Bad Request</code> with no body. Maybe it’s a CLI that says “configuration error” without naming the missing field. Maybe it’s a runtime error that shows up in the browser console as an opaque stack trace instead of telling the developer which prop they forgot or which environment variable isn’t set.</p><p>That browser console one is worth lingering on. Agents working in coding environments read terminal output and console logs. If a library throws <code>TypeError: Cannot read properties of undefined</code> when someone forgets to wrap their app in a provider component, the agent will flail. If it throws something like <code>Provider not found. Wrap your app in &lt;AppProvider&gt;. See https://example.com/docs/getting-started</code>, the agent fixes it in one pass. Same error, different surface. Every place your system outputs text is a place it can guide the agent.</p><p>Fix the worst dead-ends first. (You probably already know which ones they are. Check your support tickets.)</p><h3>2. Where can you direct agents regardless of user input?</h3><p>This is the one most teams miss. There are moments in every developer workflow where your system gets to speak, unprompted, into the agent’s context. Not because the user asked for help, but because your tooling is running and producing output.</p><p>Think about what happens when someone (or an agent) installs your package. The terminal output from <code>npm install</code> is right there in the agent’s context. A postinstall message that says “Run <code>npx your-tool init</code> to get started” is free guidance. The agent will often just do it.</p><p>Or think about what your dev server prints on startup. Most frameworks print a URL. What if yours also printed the key configuration it detected, or flagged a missing environment variable before the first request fails? <code>sanity dev</code> could print “Studio running on http://localhost:3333 | dataset: production, project: abc123.” Now the agent knows the project ID without having to parse config files.</p><p>CLI help text is another one. When an agent runs <code>your-tool --help</code> (and many harnesses do this as a first step), what comes back? A wall of flags, or a structured list with examples? The <code>--help</code> output is documentation that lives inside the tool itself. It can’t be summarized away or dropped from context. It’s always there.</p><p>These aren’t instruction-layer investments. You’re not writing docs or skills files. You’re making your system’s normal output more informative. It’s the difference between a hallway with good lighting and one where you need a flashlight.</p><h3>3. What’s the path of least resistance?</h3><p>If someone gives an agent a naive prompt about your product, what does it build? Try it. Open a workbench, type the kind of thing a new user would type, and look at what comes out. Is the agent reaching for the right packages? The right patterns? Or is it building everything from scratch because your newer, better abstractions aren’t well-represented in training data yet?</p><p>You can’t control what the agent predicts. But you can control how many paths through your system lead somewhere good. Fewer paths, better defaults, opinionated SDKs. If your product has three ways to do the same thing (the old way, the new way, and the raw API), the agent will pick whichever one has the most training data. That’s usually the old way. Deprecation warnings that suggest the replacement, clear migration guides, and SDKs that make the new way shorter than the old way all shift what “least resistance” means.</p><h3>4. Who’s evaluating the output?</h3><p>This is the one that changes the stakes. An experienced developer will catch a fragile publish pattern and go check the docs. A designer shipping their first app with Cursor won’t. They’ll trust whatever the agent produces.</p><p>The instruction layer (skills, docs, llms.txt) assumes someone who can evaluate what the agent produces. The system layer (SDKs, error messages, API shape) protects everyone, including the people who can’t. If your user base is shifting toward less experienced builders (and for most developer tools, it is), the system layer matters more than it used to. The question isn’t just “does this work?” It’s “does this work safely for someone who can’t tell the difference?”</p><h2>What I&#x27;d do if I ran a DX team rn</h2><p>Take one afternoon. Pull up your product’s error messages, your CLI help text, your API responses on bad input. Run a naive prompt through an agent and watch what happens. Then ask the four questions:</p><ol><li>What does the agent see when it fails? Find the dead-end errors and rewrite them. This is the highest-leverage change you can make, and it helps your human users too.</li><li>Where can you direct agents regardless of user input? Your postinstall messages, your dev server output, your <code>--help</code> text. These are free guidance that can’t be dropped from context.</li><li>What’s the path of least resistance? If the agent follows the most obvious path through your system, does it end up somewhere good? If not, that’s a system design problem, not a documentation problem.</li><li>Who’s evaluating the output? If your user base includes people who can’t tell good output from bad (and increasingly, it does), the system layer is their only safety net.</li></ol><p>The instruction layer matters. I’m not arguing against skills files, llms.txt, or MCP servers. But if you’re deciding where to spend your next sprint, start with the system. It’s the work that helps both your human users and the agents they’re working through. And it’s the work that compounds: every error message you improve, every CLI output you make more informative, every SDK that encodes the production-grade pattern stays improved regardless of which agent harness comes along next month.</p><p>Skills files and llms.txt are signage. Your API design is the hallway. The hallways come first.</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/51ab98ad2180a01504b7815f15309717180297e7-2816x1536.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>JAMstack_conf London: Impressions</title>
      <link>https://knut.fyi/blog/2026-03-16/jamstack_conf-london-impressions</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2026-03-16/jamstack_conf-london-impressions</guid>
      <pubDate>Mon, 16 Mar 2026 16:32:14 GMT</pubDate>
      <description>I was at JAMstack_conf London. It was great. Here&apos;s the story.</description>
      <content:encoded><![CDATA[<p>I’ve just spent the two last days being at JAMstack_conf London learning about a bunch of neat stuff, but more importantly, having met an array of great people. I have now been on three of three JAMstack_confs, and right of the bat I must say that I recommend these conferences if you’re into working with the web in some capacity.</p><h2><strong>Wait a minute, what is a JAMstack?</strong></h2><p>Having just been to a conference that has JAMstack in the title makes it easy to forget that it’s not a known concept to everyone. The JAM is JavaScript, APIs, and Markup. It signifies an approach to making websites where you use JavaScript and APIs to build your site and deliver prerendered markup to your readers. The ethos is really making sure that your website is fast, secure, easier to scale, and developer friendly. If you read this post on my domain, then you’re on a JAMstack website.</p><p>Static site generators are a significant part of the JAMstack scene. They have been around for good while. What’s “new”, however, is that there’s more attention given to the dynamic nature of the modern web. By using dedicated services with APIs and client-side JavaScript to take care of those things. In contrast to having a website directly connected to a database that has to re-render the same markup for every visitor each time.</p><h2><strong>So JAMstack_conf, how was it?</strong></h2><p><a href="https://twitter.com/kmelve/status/1148552172071731200?s=20">View on Twitter</a></p><p>Of course, I’m at JAMstack_conf not only as a participant in the community but also in my capacity as a developer advocate for <a href="https://www.sanity.io/">Sanity.io</a>. Which means that I represent a vendor in the field. This role is still fairly new to me, and there’s still the self-awarness that you are there with an agenda: You want people to try out the thing you’re working with. The nice thing about JAMstack_conf, however, is that people are curious and open, and in fact, come to you to ask questions and learn. So I just have to make sure to make myself available.</p><p>Another nice surprise with this JAMstack_conf was that I also got to meet a bunch of people that have used Sanity for a while, and that I’ve only met in cyberspace. It’s really nice to put a face on people that you have been talking to in the numerous Slacks I’m in.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/8cbcb4ba970df0bb883660faebc46d7c933240bb-720x541.png?w=800&fit=max&auto=format" alt="Jamie Bradley and Knut Melvær" /><figcaption>Middlesbrough’s own Jamie Bradley and yours truly sporting some handsome t-shirts.</figcaption></figure><p>My good pal <a href="https://twitter.com/jamiebradley234">Jamie</a> held a blazing lightning talk and gave us a solid shout out. Much appreciate!</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/fc7a87cc15f2ef36a5eb509f12e95918baa3badd-720x540.png?w=800&fit=max&auto=format" alt="Régis, David, Frank, Dan, Knut, Phil, Simon, Christopher, Gerald, David, and Rodik" /><figcaption>Régis, David, Frank, Dan, Knut, Phil, Simon, Christopher, Gerald, David, and Rodik</figcaption></figure><p><a href="https://www.thenewdynamic.org/">The New Dynamic</a> is like the lounge of the JAMstack community and some of us got into a group photo (We have to work on the demographic though).</p><h2><strong>But the talks Knut, the talks‽</strong></h2><p>The JAMstack_conf organizers usually end up finding some great speakers. They tend to fall in two categories: Those who are great at doing talks (because they’ve been doing it a bunch), and those who are maybe not as experienced but have some interesting insights. What I appricate with the scope of JAMstack is that you end up with a broad set of themes. We have the nerdy code-snippets-on-screen talks, like how <a href="https://twitter.com/compuives?lang=en">Ives</a> and his team built <a href="https://codesandbox.io/">Codesandbox.io</a> or <a href="https://twitter.com/una?lang=en">Una’s</a> inspiring session about <a href="https://ishoudinireadyyet.com/">CSS Houdini</a> (because pixelated gradient borders are awesome!). There’s also more of the higher level stuff, like <a href="https://twitter.com/simona_cotin?lang=en">Simona</a>’s systematic introduction to serverless and <a href="https://twitter.com/smashingmag">Vitaly’s</a> step-in talk for Chris (of <a href="https://css-tricks.com">CSS-tricks</a>) (<a href="https://twitter.com/chriscoyier/status/1148342580888952834?s=20">who had a bike accident</a>) about the take-aways from the JAMstackification of <a href="https://www.smashingmagazine.com">Smashing Magazine</a>.</p><p>I really appreciated learning more about web performance. I must admit this is a topic where I often feel it’s difficult to discern between what’s “web perf cargo culting”, or what is actually useful advice. I enjoyed <a href="https://twitter.com/codebeast?lang=en">Christian’s</a> lightning talk about The State of JAMstack in Africa and how to take lower-powered devices into account. It was also some good quality programming to put <a href="https://twitter.com/jaffathecake?lang=en">Jake</a> &amp; <a href="https://twitter.com/dassurma?lang=en">Surma</a> at last with their magnicifent and hilarious “Setting up a static render in 30 minutes”, in which they talked about how they made <a href="https://proxx.app/">Proxx</a> work for 2G and 3G devices.</p><h2><strong>Wasn’t you on stage as well?</strong></h2><p>We were asked by the good folks at Netlify if <a href="https://www.sanity.io/">Sanity.io</a> wanted to announce something in a lightning talk session at the conference. So we figured this would be a good opportunity to announce that we’re making our query language <a href="https://sanity-io.github.io/GROQ/">GROQ open source</a>. That meant that we had to prepare the draft for the specification for public consumption. It was also a good occasion to implement a parser for <a href="https://github.com/sanity-io/groq-js">JavaScript</a> and a <a href="https://github.com/sanity-io/groq-cli">CLI</a> that utilises it. So my colleague <a href="https://twitter.com/judofyr?lang=en">Magnus “Judofyr Holm</a> did some really amazing work creating a <a href="https://github.com/judofyr/glush">parser-parser</a> that let us set up an alpha version of <a href="https://github.com/sanity-io/groq-js">groq-js</a> rather quickly.</p><p><a href="https://twitter.com/kmelve/status/1148944749291814912?s=20">View on Twitter</a></p><p>Now, I really really really like GROQ. It was one of the things that won me over to Sanity before joining. I have done a bunch of talks and lectures before, also for large crowds. So I wasn’t expecting to be as nervous as I was. Seriously, there were quivering hands and tunnel vision. Just to be sure, I had also set myself up for some live command line querying. I did mess it up somewhat by accidentally printing 80k lines of prettified JSON, but I managed to get it back on track and show some of the query magic.</p><p><a href="https://twitter.com/SuzeShardlow/status/1148898774032539648">View on Twitter</a></p><p>My takeaway for my next talk, is that I’m not really stressed about things going wrong, but I have this thing where I start to get apologetic about things I really care about. Mainly because I’m afraid that people won’t find it interesting. And that isn’t helping anyone. So next time I&#x27;m on stage I&#x27;ll remember to channel more of my excitement.</p><p>A special mention goes out to Netlify’s <a href="https://twitter.com/philhawksworth">Phil Hawksworth’s</a> MCing that tempered my nerves somewhat — his down-to-earth and witty approach to MCing makes for a friendly and safe atmosphere. </p><h2><strong>Next time in San Francisco!</strong></h2><p>So I guess this is a flaming endorsement. If you’re interested in how you can have a better time as a web developer or a web development adjacent, do consider getting yourself a ticket for <a href="https://jamstackconf.com/sf/">JAMstack_conf San Francisco</a> in October. I’m pretty sure that the organizers will tune this one to 11. And hopefully, I&#x27;ll meet you there!</p><p><br/></p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/1122d6f661e03ac325e8c527a2d9caeb4d29c524-4032x3024.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>How I put the scroll percentage in the title bar</title>
      <link>https://knut.fyi/blog/2025-12-20/how-i-put-the-scroll-percentage-in-the-title-bar</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/how-i-put-the-scroll-percentage-in-the-title-bar</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>Add a nice hint of how far you have read a page by putting the scroll percentage in the title bar. Learn how to implement it with React and Gatsby. </description>
      <content:encoded><![CDATA[<p>Scroll down!</p><p>  </p><p>   </p><p>&nbsp;  </p><p>&nbsp; </p><p></p><p>I mean it! Just scroll!</p><p> </p><p> </p><p> </p><p> </p><p> </p><p> </p><p>And then look up. A bit further. Yes, to the area of your tab bar where the <code>&lt;title&gt;</code> content ends up. And then scroll again. </p><p></p><p>There&#x27;s a percentage there. And it changes when you are scrolling. Kinda cool isn‘t it? Now you can tab away from this site and still have a sense of how far you have read. That surely makes your existence a tad more pleasant, doesn‘t it?</p><p>The idea to put the scroll percentage in the title bar came from <a href="https://twitter.com/round/status/1251992234053885952?s=20">this tweet by @round</a>. </p><p><a href="https://twitter.com/round/status/1251992234053885952?s=20">View on Twitter</a></p><p>I didn’t make a browser extension, rather, I just put the mechanics into my own blog post template. Since I’m on Gatsby and React, it was not that hard. Here’s how to do it:</p><p>First I installed the <a href="https://www.npmjs.com/package/react-scroll-percentage"><code>react-scroll-percentage</code></a> package. That takes care of most of the heavy lifting. Then, and I’m honestly feel a bit out of depth here, I thought it made sense to put the actual mechanics into <code>useEffect</code>. It may be that it would have been possible to make Gatsby and React rerender the <code>&lt;title&gt;</code> part in another way, but I was just following my gut here (tell me on Twitter if there‘s a better way). So I ended up with something like this (<a href="https://github.com/kmelve/knutmelvaer-no/blob/master/web/src/templates/blog-post.js">actual code here</a>): </p><pre><code class="language-jsx">import React, {useEffect} from 'react'
import {useScrollPercentage} from 'react-scroll-percentage'
import Container from '../components/container'
import BlogPost from '../components/blog-post'
import SEO from '../components/seo'
import Layout from '../containers/layout'
import {toPlainText} from '../lib/helpers'

const BlogPost = ({post}) =&gt; {
  const {title, _rawExcerpt} = post
  const [ref, percentage] = useScrollPercentage()
  
  useEffect(() =&gt; {
    if (post) {
      const percent = Math.round(percentage * 100)
      document.title = `${percent}% ${post.title}`
    }
  }, [percentage])
  
  return (
    &lt;Layout&gt;
      {post &amp;&amp; &lt;SEO title={` ${title}` || 'Untitled'} description={toPlainText(_rawExcerpt || [])} /&gt;}

      &lt;div ref={ref}&gt;
        {post &amp;&amp; &lt;BlogPost {...post} /&gt;}
      &lt;/div&gt;
    &lt;/Layout&gt;
  )
}

export default BlogPost
</code></pre><p>Since react-scoll-percentage uses Intersection Observer, which is <a href="http://caniuse.com/#feat=intersectionobserver">a relatively new browser API</a>, we also need a polyfill for browsers that doesn’t support it yet. First install it as a dependency. </p><pre><code class="language-sh">npm i intersection-observer
</code></pre><p>Then we want Gatsby only to use the polyfill in the browser, and import it dynamically when it‘s needed. We’ll do that by inserting this into <code>gatsby-browser.js</code>:</p><pre><code class="language-javascript">// gatsby-browser.js

export const onClientEntry = async () =&gt; {
  if (typeof IntersectionObserver === `undefined`) {
    await import(`intersection-observer`);
  }
}
</code></pre><p>And that’s it! Now, marvel at your new updating reading lenght!</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/3b09df5d8ad1864a1277f312b61db3bc0ad66230-624x294.gif?w=800&fit=max&auto=format" alt="Screen grab of scrolling a page while the percentage in the title updates" /><figcaption>Most fun on long posts</figcaption></figure>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/e5f8a40d75dca72fe4fb15ecf7680b60a1296a2c-5184x3456.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Why I love working at Sanity.io</title>
      <link>https://knut.fyi/blog/2026-03-16/why-i-love-working-at-sanity-io</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2026-03-16/why-i-love-working-at-sanity-io</guid>
      <pubDate>Mon, 16 Mar 2026 16:32:14 GMT</pubDate>
      <description>Four years ago I was a grad student with a Ph.D. fellowship in the Study of Religions, so how did I end up working for a tech startup? This is a post about how I came to run Developer Relations at Sanity.io – it&apos;s also a celebration of following less obvious paths.</description>
      <content:encoded><![CDATA[<p>Four years ago I was a grad student with a Ph.D. fellowship in the Study of Religions, so how did I end up working for a tech startup? This is a post about how I, Knut Melvær, came to run Developer Relations at <a href="https://www.sanity.io/">Sanity.io</a> – it&#x27;s also a celebration of following less obvious paths.</p><p>(<em>Cover image: Me presenting Sanity.io on the Google stage at</em> <em><a href="https://www.slush.com/">Slush</a>, with</em> <a href="https://twitter.com/bjoerge"><em>Bjørge</em></a>)</p><p>In 2015 I was getting worn down. Recruitment practices in the academy are harsh. The prospect of being passed from one temporary university teaching contract to the next wasn&#x27;t very appealing either. My fellowship was nearing its end, and while I had both published peer-reviewed research, and done lots of teaching and dissemination – it was challenging working on a thesis using digital methods in a field with very, very little experience with such things. Also, I’d developed a severe depression – which, after a too long stretch, I sought help mastering, and am mostly OK from today (do get help people!).</p><p>A couple of months before my stipend ran out, I got a job offer as an interaction designer (which soon became “technology strategists”) at the wonderful, UX-oriented design agency <a href="https://www.netlife.com/">Netlife</a>. In addition to my tech-oriented humanities background, I was an experienced web development freelancer. But I was very lucky to get the offer as it not only meant I didn&#x27;t have to constantly worry about earning a living and how to get the next contract, but I was welcomed to a milieu where people had each other’s back and cheered you on. Not too much of that in the academy. Not that I&#x27;m bitter. Or wait.</p><p>I worked at Netlife for three years and enjoyed every minute of it – although multiple concurrent clients with high demands, sometimes on tight budgets and with much at stake, also does take its toll.</p><p>So when I got the offer from Sanity.io to join as a developer advocate, it felt like a special opportunity. I would get to work on a special technology along with some very talented people. It felt like the right time to move on and an opportunity not to be missed.</p><p>I had already had the pleasure of working with Sanity as tech-lead when building Netlife’s new website, using it as a content backend, with <a href="https://nextjs.org/">Next.js</a> as the frontend, and multiple Node.js-microservices on Heroku. We had also run the same setup for <a href="https://www.u4.no/">U4 Anti-Corruption Resource Centre</a>. Those experiences inspired me to write the blog post “<a href="https://hackernoon.com/headless-in-love-with-sanity-689960571dc">Headless in Love with Sanity</a>”, which — though I didn’t get at the time — kinda ended up being my job application. Notwithstanding, the motivation was a real appreciation to be working with something where you felt a lot of care and thinking had gone into making my life as a developer easier, and frankly, more fun.</p><p>Now that I have been working at Sanity for half a year, I thought it was time to jot down some thought about why it has been such a blast. It’s also a not too subtle pitch for those of you who either are considering applying for a position – or are asked to join. That being said, my motivation to write this it is pretty pure and an opportunity to reflect on what makes my job meaningful and exciting.</p><h2>The people</h2><p>I, like many others working with technology in Norway, had an admiration the people behind Sanity for many years. They used to be a technology- and design agency called <a href="http://bengler.no/">Bengler</a>, with a track record for doing weird/challenging/state-of-the-art work, such as making a <a href="http://bengler.no/origo">social network tied to local publishing</a>, that launched concurrently with Facebook, <a href="http://bengler.no/grbl">controller software for 3d printers</a>, <a href="http://bengler.no/chorder">a chorded text input device</a>, or <a href="http://bengler.no/panda">disruption-as-a-service toolkit</a>.</p><p>Frankly, coming to work with these people would have been terrifying, if it weren’t that they are all incredibly pleasant, grounded, and open-minded. They challenge you when you haven’t thought properly through the implications of your suggestions, but also cheer you on when you ship. As one of the first “outsiders” in the new company, I have never felt excluded or not a part of the team. Of course, that may come from selection bias, but I would be surprised if the same didn’t account for new, and more diverse hires than myself (a Norwegian white male in his 30s). It&#x27;s a startup with grown-ups, with families, obligations, hobbies, and things happening outside of work.</p><h2>The product</h2><p>At work, pretty much all of our attention is tied up in what we’re making: Sanity. I have done my fair share of CMS related development, be it Wordpress, Craft CMS, Statamic, Contentful, and others. Although it isn&#x27;t fair to categorize Sanity as “just another CMS,” used as one, it has undoubtedly been the most fun one to work with (and Craft CMS is pretty darn fun). I believe it’s because Sanity.io opens you to so many possibilities. There are plenty of doors that open when you deal with a real-time backend, having a relational document store with a graph-oriented query language, with <a href="https://github.com/sanity-io/sanity">an open source React content editing app</a>. This makes Sanity.io pretty unique and awesome – it took them years to build.</p><p>However, all the little things are as important: some minor functionality in the CLI, some available method on the way you can tweak the document listings or a feature in GROQ. I can’t count how many times I asked if you could do this or that, and while waiting for the answer, discovering that, yeah, you can do it. It&#x27;s nice to stumble over exactly what you need when you need it. Using Sanity.io has been a series of such experiences.</p><p>Sanity is already remarkable in its own right, but looking at the very secret backlog, there are so many cool things that we can build on top of this stack, that I can’t even. 2019 will be an exciting year, for sure.</p><h2>The community</h2><p>I have been mistaken so many times since I started in Sanity – which to me, is part of the appeal (that means that I’m learning). One thing I was very wrong about, was my worry about launching the community platform. I was so ready to have to moderate and deal with trolls and hostile people in our <a href="https://slack.sanity.io/">community Slack</a>. I was very wrong.</p><p>It has been such a joy to get to know our community, get to answer surprisingly challenging questions (because people are using Sanity to solve challenging things), take very constructive feedback, and — this is my favorite thing — see the stuff people are building. The community has grown very fast, but it hasn’t been a single instance of harassment or bad behavior (as far I have been able to gather). As we continue to grow, a key concern is to make everyone feel welcome and included.</p><h2>The office space</h2><p>I go to work to the coolest office (though the floors are heated). Almost every day (as a commuter I treat myself to the home office once in a while), I enter a converted horse tram depot at Grünerløkka, a lively neighborhood in central Oslo (the capital of Norway). It’s an open space, yet it doesn’t give the feel of an open office. Behind where I tend to sit, there is a workbench where <a href="http://polarworks.no/">Polarworks</a> (which also grew out of Bengler) are developing super cool motion control software. It very much feels like a maker space. So although I’m not that into beer and aware that it makes us sound like outtakes from Portlandia, it is indeed nice to have a micro-brewery where good tasting brews are made. Next year we&#x27;ll be opening an office in San Fransisco, that&#x27;ll be exciting too!</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/a9aa41bfb84fee69fa42362164959cbd5f31c539-880x660.png?w=800&fit=max&auto=format" alt="Person presenting in front of a crowd" /><figcaption>From one of our meetups</figcaption></figure><h2>The challenges</h2><p>To me personally, the primary motivation that brings me to work every day is many challenges of working on a product like Sanity, which both has many features, but with the ethos of being super easy to use. We sincerely believe Sanity has a place for developers that need a backend for structured content – be it a weekend project, or for enterprise needs. To take a product out in the world is hard work. To prepare your organization for taking on more people is always a challenge. To understand what’s truly useful for people, and how to communicate with them, is not easy. To take time off to recuperate and get enough brain air to stay creative can be difficult when crazy-stuff-we-can’t-talk-about-yet™ is happening every week. To be allowed to be wrong, to experiment, and given the trust to try stuff I haven&#x27;t really done before are sometimes scary, as there is a great deal of responsibility that goes into it.</p><p>However, those are also the things that drive me, and why I, frankly, love working for Sanity.io. I hope I get to share that pleasure with you some day.</p><p><em>I should also mention our office dogs, my own Jara, and</em> <a href="https://twitter.com/rexxars"><em>Espen’s</em></a> <em>Kokos.</em></p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/89491eb88d6ed360a3c5e59ebe57c035f41243bc-880x585.png?w=800&fit=max&auto=format" alt="A whippet puppy lying on a blanket" /><figcaption>Jara (📸 Even Westvang)</figcaption></figure><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/fafb5bb31d1d249df09a2b24c3a32400930ed6eb-880x587.png?w=800&fit=max&auto=format" alt="A dog lying on the floor" /><figcaption>Kokos (📸 Even Westvang)</figcaption></figure>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/d4a3bca8cce5e005149350a4d85e1ea573b722ed-1000x420.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>“Every day, somebody&apos;s born who&apos;s never seen The Flintstones” – or, why telling it once isn&apos;t enough</title>
      <link>https://knut.fyi/blog/2025-12-20/every-day-somebody-s-born-who-s-never-seen-the-flintstones</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/every-day-somebody-s-born-who-s-never-seen-the-flintstones</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>Most things bear repeating.</description>
      <content:encoded><![CDATA[<p>If you are a content creator you probably want people to know about your stuff. Especially in Developer Relations, where most of our content is supposed to be useful and educational, we do want to make sure that people at least know there is some content that might be interesting or useful as they&#x27;re getting more familiar with the platform.</p><p>It&#x27;s surprisingly easy to get into the habit of publishing some content along with some initial tweet(s) and a post in various communities and move on to the next thing. But chances are, the content you just pushed into the world is long-lived and interesting for way more people than saw your initial push.</p><p>At least twice a week I think of the paraphrase that <a href="http://merlinmann.com/">Merlin Mann</a> frequently does on his podcasts, “Every day, somebody&#x27;s born who&#x27;s never seen The Flintstones.” I don&#x27;t remember the full backstory (I believe it was a program manager at a local radio station) and the cultural relevance of <em>The Flintstones</em> is probably not prevalent.</p><p>Even so, I find it a useful reminder that there are always people:</p><ol><li>who don&#x27;t know the things you may take for granted as “common knowledge.” This is especially true for programming and product topics.</li><li>who haven&#x27;t seen your content yet</li></ol><p>So what are the practical implications?</p><p>There is practically no lack of ideas or things to create content on. Even the things that might seem obvious. Of course, there might still be more or less intriguing ideas, but thinking that “everyone knows this,” should never stop you.</p><p>Whenever you plan to release some content or do a launch, also plan for how you want to revisit this content down the road. Schedule tweets for later, add them to the rotation of tips and tricks to be posted in your community. – your future self will thank you.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/61baa16bf73a67d55a06719724497565c8f42313-462x316.png?w=800&fit=max&auto=format" alt="Saying 'what kind of an idiot doesn't know about the Yellowstone supervolcano' is so much more boring than telling someone about the Yellowstone supervolcano for the first time." /><figcaption>https://xkcd.com/1053/</figcaption></figure><p>P.S. And yes, even the points made in this post have been done many times before. Full meta.</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/51ca95e0bbc11e09827324211351d62cbb1a4256-1280x720.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Lies, damned lies, statistics, and developer experience rhetorics</title>
      <link>https://knut.fyi/blog/2025-12-20/lies-damned-lies-statistics-and-developer-experience-rhetorics</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/lies-damned-lies-statistics-and-developer-experience-rhetorics</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>How should we reason about W3Tech’s web technology stats when advising on how to build on the web? And maybe it’s OK to use React, Tailwind, or any other hyped technology, to get work done?</description>
      <content:encoded><![CDATA[<p><em>Watching Rich Harris’ recent talk on “<a href="https://www.youtube.com/watch?v=uXCipjbcQfM">Frameworks, the web, and the edge</a>” reminded me that I had this blog post in drafts. So here it goes. My apologies to everyone I might wrong here.</em></p><p>A while back, <a href="https://andy-bell.co.uk/the-extremely-loud-minority/">Andy Bell offered some thoughts on silent majorities and whether you should feel technology FOMO from “a very loud minority.”</a> Specifically, to what extent do you, as a (web) developer, need to feel the pressure to learn React, Vue, or TypeScript? Because if you log on to Twitter and follow the trades, it seems everyone is using those nowadays, right?</p><p>Andy’s argument seems to be that you can safely ignore the pressure of learning these tools since, according to <a href="https://w3techs.com/technologies">W3Tech’s statistics</a>, React, and Vue only make up 4% of the web, the majority being WordPress, with a booming at least 43.6% of the top, publicly available, websites of the web<sup>[1]</sup>.</p><p>In other words, if you’re following the discourse on web development on Twitter, you’re not hearing from most who are building the Web, which is a way to say that you don’t need to put that much weight on their opinions.</p><p>In Andy’s own words:</p><blockquote>Always remember that although a subset of the JavaScript community can be very loud, they represent a paltry portion of the web as a whole. […] Now when you look at it like that, it makes you wonder why we give these people such a large stage while the very quiet majority don’t get a voice at all. The very quiet majority are out there building more than 90% of the web, after all.</blockquote><p>I’m not sure we can use these stats like this, and I’m not sure this is the way to fight gatekeeping either. </p><h2>But first, we need to set the stage</h2><p>Before you read on, I feel I have to be clear: I am not here to defend React, TypeScript, JavaScript, or any specific technological preference. My agenda here is to bring a bit of nuance to what I feel is a fraught discourse in web development and, hopefully, contribute constructively to this conversation.</p><p>I think it’s great and important that we have professionals like Andy, who advocate for the web platform as an accessible (in the broadest sense of the word) place to do work. But I also think it’s great to have educators like Kent C. Dodds (one of the people quoted without attribution in Andy’s blog post) who have done a lot to introduce people to the discipline of professional web development. Andy and Kent have different approaches and preferences regarding tooling and focus, and this diversity ultimately a great thing that enables the conversations that bring us forward.</p><h3>And a little bit about me</h3><p>I have been around on the web long enough that I was first introduced to style attributes and presentational HTML (looking at you <code>&lt;center&gt;</code>) as the way to design a web page. I have used clear fixes and tables for the layout as the <em>idiomatic</em> way to do exciting stuff that got you noticed because it pushed the web beyond a boring document viewer for CERN engineers. </p><p>And it’s not like “frontend hype” is new. I have built <em>many</em> WordPress sites since I first tried it out shortly after its introduction in the early 2000s. I was one of many who fell for the jQuery hype back in the day. And I shipped sites using LESS, Sass, SCSS, BEM, grunt, gulp, MooTools, CoffeScript, Pug, Bootstrap, Foundation, and other patterns and frameworks that have popped up on the scene for the past 20 years. And in 2015ish, I started using React to build an internal Spotify analytics tool for Warner Music. It was actually then I <em>really</em> started learning JavaScript and more patterns from functional programming.</p><p>But more importantly, I have met thousands of developers who use these technologies to solve problems for their clients and customers and to earn a living. The kind of developers, like Andy points out, who aren’t necessarily blogging about bundle sizes or being loud on Twitter. So having established my <a href="https://en.wikipedia.org/wiki/Ethos#Rhetoric">ethos</a>, let’s get into it.</p><h2>Statistics are just numbers with a narrative</h2><p>Nothing makes you doubt statistics as studying statistics does. You’ll find this quote by Benjamin Disraeli (made popular by Mark Twain) in every other statistics introduction book: “There are three kinds of lies: lies, damned lies, and statistics.” Statistics can help us understand landscapes and connections that are usually beyond what we can intuit, but numbers and graphs can be incredible rhetoric persuasive devices. Building your argument on statistics requires you to trust how these numbers were produced. One usually does that in the academic world by being very transparent with the data collection, methodology, caveats, and degree of uncertainty we’re dealing with.</p><p>My statistics professor used to say that a “proven” (by falsifying the null hypothesis with a mutually accepted degree of repetitive uncertainty) correlation is only interesting if it comes with a reasonable explanation for why it can occur (yes, I know this can be discussed, he was being didactic). I think the same can be said for sampling and framing of stats. If you want to use statistics to build your stance in an argument, you have to make a reasonable explanation for why the statistics are relevant and prove your point. I don’t feel Andy and similar takes do this. Yes, that goes for lighthouse scores too. </p><p>To be honest: I’m unsure what to conclude from W3Tech’s stats when choosing what technologies to recommend and prioritize when creating education and making topics for discussion and critique. If the W3Tech stats proves anything, it’s the adoption inertia of web technologies. That goes for both WordPress and React. </p><h2>What is the W3Tech stats representative for?</h2><p>According to W3Tech, they crawl the web by downloading the web pages and running them through static analysis using regular expressions and DOM traversal. They have built-in rules that allow them to assume server-side technologies as well. For example, if you can identify a WordPress site, you can safely assume that it’s using PHP on the server. This seems reasonable, although we don’t get much more insight since W3Tech is a commercial and proprietary operation that sells more fine-grained insights. In fact, <a href="https://q-success.com/about">it’s a 2-person company called Q-Success operating from Austria</a>.</p><p>I think it’s reasonable to assume that their numbers are representative of the technologies that are used for <em>the accumulative publicly available web</em>. This means that these numbers tell a story about technology choices that were made up to several years ago, and only for the stuff that isn’t hidden behind login forms or is running in browsers but not on the open web. It’s still important, but it doesn’t tell the whole story.</p><p>Because the argument Andy is making isn’t about what runs on the publicly available web, it’s whether you, as a web developer in 2023, can safely ignore advice and pressure in learning React and TypeScript based on the induction of what most web developers are working with. At least, that’s how I make sense of his post and the broader conversation that Andy is part of. Who are typically very critical (and sometimes rightly so) of React and similar client-side JavaScript frameworks and the impact they have on the web’s user experience.</p><p>These stats don’t tell us what’s expected from web developers when it comes to what proficiencies are mentioned in web developer job descriptions, everything that runs behind a login screen in the ever-growing personalized web, the wealth of internal web applications, and the aspirations of web developers looking what technologies they want to learn and put to use.</p><p>And if we chose to use these numbers to anticipate the technology choices of a silent majority, then we absolutely should be teaching and talking about React. Since it&#x27;s become a core technology in WordPress’ block editor, Gutenberg. Or new WordPress adjacent technologies like <a href="https://faustjs.org/">Faust.js</a> (also React). Of course, it can be discussed if web developers who use WordPress <em>need</em> to use it headless or build/customize the block editor, but you see where the arrows point. </p><h2>Stories told by other types of statistics</h2><p>What I find confounding with how the W3Tech’s stats were used is that they tell a different story than most of the annual surveys on web technologies trends. If you look across surveys like <a href="https://2022.stateofjs.com/en-US/libraries/front-end-frameworks/#front_end_frameworks_experience_linechart">State of JS</a>, <a href="https://survey.stackoverflow.co/2022/#most-popular-technologies-webframe">Stack Overflow</a>, <a href="https://octoverse.github.com/2022/top-programming-languages">GitHub</a>, etc. Generally, they are all telling the same grand story:</p><ul><li>JavaScript is used a lot for web development</li><li>TypeScript is growing a lot, and people want to learn it</li><li>React-based frameworks are dominating, but other frameworks are relevant too</li></ul><p>Now, these surveys come with their own challenges regarding sampling biases, but collectively it’s reasonable to assume that they represent a significant portion of web developers. At least relevant enough not to be outright dismissed and ignored when thinking about what frameworks to discuss and make education for.</p><p>We also see similar tendencies in my day job at Sanity.io. The search volume that is people looking for resources on topics like React, Next.js, and TypeScript can’t be ignored. Content creators and educators on YouTube, Twitch, and so on cater to the same type of demand. FreeCodeCamp’s “<a href="https://www.youtube.com/watch?v=bMknfKXIFA8">React Course - Beginner&#x27;s Tutorial for React JavaScript Library</a>” from March 2022 has, as of today, 2.3mill views. That’s 43% of the views in a year compared to the 4-year-old “<a href="https://www.youtube.com/watch?v=mU6anWqZJcc">Learn CSS and HTML from scratch</a>” video with its 5.3mill views.</p><p>Again, I’m drawing on these numbers to make a point. The above stats are nothing to the 4-year-old introduction to Python with its 38mill views. Which tells a different story about what developers and programmers are into and what demand we see in the market. Does that mean that we all should be talking about Flask and Django instead?</p><p>I didn’t find any good stats on the type of web development competencies mentioned in job descriptions. But scrolling through web development jobs on indeed.com, you’ll find JavaScript frameworks like React mentioned frequently. I also tend to see it mentioned on developer CVs and applications I’ve read. Still, this is anecdotal, and it’s uncertain how representative this picture is for the population of web developers. But I would suggest that it at least justifies recommending learning these technologies if you’re interested in a career in web development.</p><h2>Fighting gatekeeping with gatekeeping</h2><p>I think Andy takes the W3Tech stat interpretations too far in dismissing the value of opinions about learning TypeScript or “best practices don’t work.” But I think he is right regarding the amount of FOMO you should feel and not just taking the opinion of popular personalities on Twitter at face value. As a web developer, you will probably get a lot back from investing time in learning the basics of the web platform and building your understanding of what goes on behind the scenes of React, Tailwind, and so on. Knowing JavaScript well will make you a better React developer, so it’s not a zero-sum game, either.</p><p>Even though Andy isn’t mentioning it explicitly in the blog post, it can be read as an entry in a conversation about gatekeeping and heightening the bar to web development. I understand and support the desire to speak out against gatekeeping, but I often find that it’s done in a way that outright dismisses some legitimate and empathetic advice. </p><p>For example: Telling someone to “not ship JavaScript to browsers” or “don’t use Tailwind, just learn CSS” is not particularly empathetic or helpful. Rather, telling someone that Tailwind can get the job done quickly, but it’s possible to write it in a way that makes it harder to maintain, is actually useful advice. Or that a JavaScript framework can come in handy if you plan to make an interactive e-commerce experience without having to deal with backend programming languages, but you should keep an eye on different performance characteristics in the kind of browsers and platforms that the customers run the site on.</p><p>I also think it’s a stretch to assume that adopting TypeScript, or most other frameworks, makes it harder to enter web development in the first place. If you look at how modern web applications are built, and especially if you’re looking to do anything server-side, then yes, the initial learning curve might be steeper, but I can tell you that it’s significantly easier to be onboarded to a well-done TypeScript project versus a well-done JavaScript/Node.js project. And it’s not like it’s especially easy for a beginner to get started with WordPress locally either (more moving parts, different programming languages). </p><h3>Beware of The Ideology of The Holy Web Trinity</h3><p>The inclination that recommending any technology beyond the holy trinity of HTML, CSS, and (vanilla) JavaScript is gatekeepy, confounds an idealized stance about how to do web development with the very real situation a lot of aspiring web developers are facing. These advocates, in their rhetoric, dismiss that something like the React ecosystem empowers developers and teams who are asked and paid to solve problems. Wanting to change the direction of the web is built is a perfectly legitimate stance, but it’s probably not helping the case to dismiss voices that happen to promote tech you don’t like as “loud” and hence, not interesting to listen to.</p><p>If you want to make a strong case against popular technologies, it’s probably more effective to communicate an understanding of why these are used, and then provide viable alternatives. And not to undermine your audience: In most of my conversations with developers, I see curiosity towards things that are “hyped” co-exist with a healthy skepticism for what technology to put in production.</p><p>Having done developer marketing for five years, I know that the most efficient marketing is to show developers how to solve real problems without hyperbole. You’ll find much of that in the React ecosystem (take <a href="https://tanstack.com/">the marketing of TanStack</a>). Even Vercel’s Apple-like marketing of Next.js is grounded in narratives of solving actual problems (and their style doesn’t escape critique either).</p><p></p><hr /><ol><li id="fn-8b189eb7c219"><p>This is quoted, <em>so many times</em>. </p></li></ol>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/0c80ac90e06c7ed986dcb578ec9636616ca85387-6016x4016.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Counting hours (or; how to work less when you want to work much)</title>
      <link>https://knut.fyi/blog/2025-12-20/counting-hours</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/counting-hours</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>I love what I do for work and I spend a lot of time on it. But how to balance inner-drive with ideals about work-life balance?</description>
      <content:encoded><![CDATA[<p>I have worked a lot for the almost 2 years I’ve been at <a href="https://www.sanity.io">Sanity.io</a>. Evenings, weekends, you name it. I know what you’re thinking: Another example of the exploitative tech startup churn.</p><p>But I’m not working a lot not because anyone has told me to do so. Rather on the opposite, I probably have at least 5 people, and among them mostly my managers telling me on a weekly basis that I should work less. The culture at Sanity.io isn&#x27;t that you should <em>crunch it</em> on weekends and late hours, but rather, take time to plan and think on how to use the time one has on the right things. To get more done in less time with fewer distractions. </p><p>I don’t put in a lot of hours because I feel I have to prove myself either. Or because I’m hunting a promotion. I’m in the very privileged position where I’m fairly confident that I’m good at my job, and comfortable with the fact that I have a lot to learn. I put in a lot of hours because I inherently enjoy what I do. I look forward to it. It’s fun. It’s challenging. I learn a lot. </p><h2>The hegemony of work-life (un)balance</h2><p>It must be said, that this deep enjoyment is a relatively new thing for me. I have been at places where I have allowed myself to be consumed by work for all the wrong reasons. I have been through burn-out and depression, and used work as an escape from working with my own mental and emotional health and my tending my relationships. The stuff that <a href="https://lengstorf.com/blog/tag/worklifebalance/">Jason Lengstorf is writing about on his blog</a>. Or <a href="https://basecamp.com/books/calm">Jason Fried and David Heinemeier Hansson over at Basecamp</a>. (I realize that this paragraph doesn&#x27;t pass the <a href="https://en.wikipedia.org/wiki/Bechdel_test">Bechdel test</a> - <a href="https://twitter.com/kmelve">let me know</a> if you know of someone that should’ve been included. Edit: <a href="https://www.ted.com/talks/arianna_huffington_how_to_succeed_get_more_sleep">I got recommened Ariana Huffington‘s talk about sleep</a>)</p><p>There’s a push towards healthy work-life balance in the tech discourse, especially as a response to predatory practices and unreasonable expectations in the start-up world. VCs that suggest that getting a business up and running (with the kind of growth that’s expected in this part of entrepreneurship) may take a bit more effort than a 40 hour week will get hammered by people who thinks this is setting an unhealthy precedence. Both arguments have merrits.</p><p>Even if you remove high expecations of growth and insert somber Scandinavian values to work-life, building a company and a product will require more time and energy of the people involved compared to what you typically have in a “normal position” at established companies. That makes it all the more important that you create a culture where people can endure for the laung haul, and not hit the wall. And that is my experience of what one tries for at Sanity.io.</p><p>So I am not expected to be available on chat at all times (you set yourself away in focus mode when you need to get deep work done). Meetings are recognized as being expensive in terms of people’s time and attention, thus we try to come to them well-prepared and setting “hard-stops” is normal. If there isn&#x27;t an agenda or something to actually discuss, we are happy to just call the meeting off.</p><p>So the drivers behind me putting in a lot of hours isn&#x27;t found in the culture or expecations that surrounds me, it’s coming from within.</p><h2>Vocation versus having a job</h2><p>I do believe it’s crucial to take time to be someone for your family, partner, kids, friends, or pets. To exercise, sleep, taste things, travel into fiction, take part in culture, listen to music, work with your emotional pain, muse about your existence, and seek spiritual experiences (whatever they are for you). Work can get in the way of that. But sometimes the Venn-diagram between work and life will overlap. And then the dichotomy doesn&#x27;t fit.</p><p>What I’m getting at is the idea of having found a vocation versus having a job. I suspect that many startups begin with founders and early-stage employees that very much has building a product and a business as a vocation, a calling. It might sound a bit precious, but I think you’ll often find the same drive among teachers, athletes, doctors and nurses, and in religious professions (from where the word originates).</p><p>The problem arises when you as a founder or early employee that has now entered into a mangager role, assume that it’s reasonable to expect the same calling or priorities from others. It’s probably cool if they do, but I believe you will be much happier if you accept that people can do great things in the hours you get to spend with them, and still turn off e-mail and Slack notifications to go home to be a dad, a partner, or to something completely different. </p><p>Now that I&#x27;ve entered into a manager role, I’m also worried that my behavior is modelling expectations that I expressively don’t have: I fully understand that other people that have other priorities for what they want and have to spend time on. In fact, I expect it to be a requirement for bring their best selves to work. I have no problem adjusting and planning for not being able to get in touch with a colleague after office hours.</p><p>If you find yourself in having a vocation in a job that you enjoy. That’s great! But remember that’s a privilege that you get to enjoy, and not a reasonable expectation that you can put on others. And to be honest, I find it much more inspiring to work with people who bring other forms of nerdery, interests, and hobbies to the table. </p><h2>I need to be at work less</h2><p>I probably have to do something with my own behaviour and prepare to be less present outside of working hours. I should reduce my activity on Slack in weekends, and be more mindful about when I engage my teammates in work-related discussions during evenings. And remember, we&#x27;re talking about <em>patterns</em> here, not absolutes. </p><p>I brought my dilemma up to one of my managers: “I keep being told by all of you to work less, but I don&#x27;t see that I&#x27;m really changing my behaviour. And I don&#x27;t feel I’m on the path to burn-out, but I&#x27;m worried by the precedence it may set. So. How should we approach this differently?”</p><p>The advice I got was to approach my days more mindfully by do more planning, to think about what is really important, and to try to measure what I’m actually spending time on, like, in hours. These are activities that I traditionally haven’t eager to do. But as I have gotten more responsibility, I have slowly started to appreciate them more.</p><p>I already do fairly more planning. Both long-term, my upcoming week, my next day, and my day. I keep lists. It has helped me being less reactive, and I do less things that may feel urgent, but is probably not important. But spending as much time on more of the right things is only half of the challenge.</p><p>So I need to start with the same strategy as when you want to manage your weight, but instead of measuring how much energy I consume, I have to measure how many hours I spend on what things. I need to build some habits that doesn&#x27;t depend as much as my immediate motivation, and set myself up for success for what I want to achieve. I must admit I kinda dread having to dedicate part of my cognitive brain to keep tally of what I’m doing, but all the better reason to do it probably.</p><h2>Know thyself; start smol</h2><p>I know that I probably will continue to thinking about new ideas at any hour, get the urge to get stuff down on paper, roam the web for inspiration, and all these things that you do in knowledge work. My main worry is that I’m modelling behaviour and unreasonable expecations for others, so what I need to fix first is how available and present I am: That begins with being off Slack during weekends. </p><p>Since I do a lot on US time (whilst living in Norway), I will also try to log onto work later in the morning, using the first few hours of the day to read, get some exercise, do chores, plan holidays, and so on. And the big one: I will also try to get into time tracking, and probably start small, by tracking just one type of activity, and take it from there.</p><p>These things are relatively small behavioural changes that I should be able to make happen. I have tried to make sure that the hardest one, starts with relatively low expectations. Start time tracking just one thing. Is the same as starting to run: It’s much easier to drag yourself out to run for 15 minutes than an hour. And getting out the door is the hardest part. </p><p>So this is me acknowledging that there is a door, and a run to be had. And that’s the first step.</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/fa6cc48eec1013a5de31b73d95b668806063aac1-4149x3235.jpg" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Once more, with feeling</title>
      <link>https://knut.fyi/blog/2025-12-20/once-more-with-feeling</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2025-12-20/once-more-with-feeling</guid>
      <pubDate>Sat, 20 Dec 2025 18:01:17 GMT</pubDate>
      <description>I&apos;m excited to announce five tips for taking your marketing copy to the next level</description>
      <content:encoded><![CDATA[<p>The next time I read marketing copy that introduces with “we’re excited to”, I’ll go through the motions of finding a printer, installing whatever drives and needless printer vendor software is forced upon me, buying some real nice 80gms A4 paper bought from a local arts and crafts shop, print it out the piece, dunk it with gasoline and set fire to it. </p><p>Because that’s the only way to make that phrase truly relay excitement.</p><p>You&#x27;re still reading? OK, good. Let&#x27;s talk about writing great marketing copy!</p><p>After I started working with marketing in a startup I’ve become more attuned to how the craft is being done. And for some reason, a lot of announcement posts start with the phrase “we’re excited to” followed by something that doesn’t smell of excitement at all. Anyone who has caught on to this might be tempted to replace “excited” with “thrilled,” “proud,” “elevated,” or even “stoked.” </p><p>But the verb isn’t the problem. It’s that you are too focused on the what and not the why. What you’re up against is not people just waiting to hear the latest from your company. They want to be heard, understood and cared for. And your job is to figure where their desires and your news overlap and take it from there.</p><p>No one sits on the toilet scrolling to figure out how to optimize efficiency for their teams.</p><p>You are probably working on something that’s worth getting excited by! Your team has probably made something that’s useful, and even super interesting and impressive (if that’s not obvious, then you might have deeper problems). But you are also oh so busy, you have shit to do, that needed to get out like yesterday! </p><p>So we churn out marketing copy leaning on the words, phrases, and structure that we’re used to to get it done. We write things about “teams,” “collaboration,” “efficiency,” &quot;innovation,&quot; and “transformation” that will never spark an ounce of dopamine with anyone.</p><p>If your job is to persuade people to pay some attention to whatever you have going for you and take your product into consideration, then you need to consider what makes people stop whatever they are doing, be intrigued enough to pay attention and make the decision to take in what you communicate.</p><p>There are a couple of tips that I tend to give to my team:</p><ol><li>Remember who your audience is and what they care about. Why should they care?</li><li>Explicitly recognize the problems they have (that you can solve)</li><li>Use tension, conflict, drama to get people hooked by putting emotion on the problem</li><li>Give them a call to action early on</li><li>If you manage to get someone hooked within the first paragraph or two, you can almost do whatever with the rest of your text</li></ol><p>Of course, you might have heard some variation of these tips before. It’s rather basic if you think about it. But if it was truly that easy, people would be doing it more. So if you want to give yourself a competitive advantage: Take your draft and do it once more, with feeling. C<em>on brio! </em>(that&#x27;s Latin for “with spirit”)</p>]]></content:encoded>
      <author>Knut Melvær</author>
      <enclosure url="https://cdn.sanity.io/images/ndjrels0/production/a055a2278dea83ffdec3367d7d32da5f5e422c92-1271x1099.png" type="image/jpeg" length="0" />
    </item>
    <item>
      <title>Through the uncanny valley of AI</title>
      <link>https://knut.fyi/blog/2026-01-26/through-the-uncanny-valley-of-ai</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2026-01-26/through-the-uncanny-valley-of-ai</guid>
      <pubDate>Mon, 26 Jan 2026 01:45:15 GMT</pubDate>
      <description>I prompted an eslint plugin into existence in a day. So why do I feel weird about it? On the strange mix of empowerment and nihilism in the AI era.</description>
      <content:encoded><![CDATA[<p>“What will “developer marketing” even mean at the end of 2026?”</p><p>As someone with “developer marketing” in my job title, this question is partly existential<sup>[1]</sup>. Not since I was digging myself deep into the academic world of the humanities in my mid-20s have I felt this strange mix of urgency, creativity, frustration, and intellectual fatigue.</p><p>Especially when Claude Code and Opus 4.5 (aka <strong>Clopus</strong>) entered the scene. Last time I felt this energy was when stuff like Express.js, Flexbox, and React came about and got everyone excited and frustrated.</p><p>My former colleague and developer-education extraordinaire, <a href="https://www.simeongriggs.dev/">Simeon</a>, told me he increasingly views his job as teaching <em>agents</em> how to use PlanetScale. When talking to some of the brilliant engineers I know, they report that even with Opus and Claude Code, it’s a mixed bag: some get a lot of mileage out of it for certain things, while for the more specialized or less represented problems in the training, it’s not working as well for them.</p><p>And then they also tell me about the <em>experience</em> of skill decay and that it removes the joy of figuring out a problem in code. While others tell me that it removes everything they don’t like about coding.</p><p>I can relate to all of it. And it’s making my head spin.</p><p>When my head spins, it has always helped to write about it.</p><p>This is what you are reading now.</p><p>It feels like we’re traveling through an uncanny valley between two opposing mountain sides, looking to our left, there is all of this amazing opportunity to be discovered, and looking to our right, there is pain, disruption, and uncertainty.</p><h2>The side of hyper-creativity</h2><p>On the sunny side of this valley, you walk the paths from ideas to proof of concepts with the greatest ease. You find yourself doing stuff with programming languages and technology that you didn’t before. Because the cost of entry was to darn high.</p><p>I was able to prompt a schema-aware eslint plugin, <a href="https://github.com/sanity-labs/sanity-lint">Sanity Lint</a>, that wraps a colleague’s Rust code into WASM into life in a day (while doing my day job). This would have taken me at least a week or two in the before days. It even set up the npm publishing pipeline for me. Which I never want to do manually ever again. It feels very empowering. </p><p>I made it build a private finance planner tool on top of a csv of transactions.</p><p>I started making something akin to <a href="https://clawd.bot/">Clawd Bot</a> myself over the holidays, but got stuck in the bureaucracy trying to get a phone number to work with Twilio (yes, would have been trivial to set it up with Signal, but I wanted to just iMessage it).</p><p>Clopus feels so powerful that I almost feel guilty for not having more ideas to constantly throw against the large language model wall.</p><p>I can offload a lot of the boring bits of my day-to-day marketing work, like gathering and organizing research, project reporting, and so on, and focus on the creative parts.</p><p>And with <a href="https://miriad.systems">MIRIAD</a> (an agent orchestration tool I’ve been using, vibe-coded by <a href="https://github.com/simen">Simen Svale</a>), I can deploy a small team of specialized agents and have them figure out a lot of the stuff between them, instead of me running around with markdown files, doing context management for the clankers.</p><p>In this mode, I find that what I bring to the table is providing the right cues and input to make the LLM figure it out quicker and better.</p><p>I also provide the “taste,” that is, knowing what good looks like.</p><p>When this work with LLMs “clicks,” it feels exhilarating and powerful.</p><p>It’s democratizing (for those who have the means to pay for tokens) in allowing more people to create with technology that only a couple of years ago felt beyond their reach.</p><h2>The side of creative nihilism</h2><p>But there is the other side of the valley where the shadows lurk. It’s the sneaking feeling that everything I spent two and a half decades learning is now… pointless? At least practically. It’s not like AI took away the joy I once felt when I figured out how looping over arrays work, or when I finally grasped the mental model of what a <em>callback</em> is.</p><p><strong>But if anyone in principle can make something like <a href="https://github.com/sanity-labs/sanity-lint">Sanity Lint</a> (the eslint plugin I mentioned) if they need it, why should I bother?</strong></p><p>It also feels that so much of the engineering we’re doing around the LLMs is so temporary. We’re basically just figuring out how to put markdown files with instructions in the right place at the right time. And finding new ways of infusing the somewhat outdated model on how to write React anno 2026. It was “custom GPTs,” “projects,” MCPs, “sub agents,” and this month, it’s all about “skills.”</p><p>So it feels a bit pointless to dive too much into these techniques, because they will certainly be gone within a year, hopefully folded into abstractions where LLMs are orchestrated to understand the semantic field of your ask, and just load into context what they need to know about.</p><p>AI feels like toothpaste you can’t get back in the tube, but that was built by breaking the (western) cultural contract of intellectual ownership of your creations. It’s a bargain we didn’t consciously make.</p><h2>Before the AI hike: some backstory</h2><p>Back in the early 2010s, I was on a PhD fellowship trying to bring the digital humanities into the study of religion. My research asked if the huge corpus of digitized Norwegian newspapers from the 1700s until today (yes, really) could be used to tease out how the public used and thought about concepts like “religious” and “spiritual.”</p><p>Starting out, I had to look to computational linguistics for methods to run through the 70,000 articles that mentioned these concepts. I taught myself Python and became pretty good at regex. I installed undocumented Java applications to run statistical models. There was a lot of banging my head against the wall.</p><p>But all the methodologies were pretty much founded in the same principles: identify the word (aka “token”), decide how many words before and after you want to look at, and run your usual suspects of statistical methods (t-tests, chi-square, ANOVA, and so on) in various creative and layered ways.</p><p>For someone not trained in computational linguistics, discovering that these methods boiled down to counting word occurrences and calculating averages and variances felt like a dead end.</p><h3>From means to fields…</h3><p>And then I found folks working with <a href="http://en.wikipedia.org/wiki/Kernel_density_estimation">kernel density estimation</a> to map <em>semantic fields</em> of a corpus. Going from deductive statistics to inductive statistics let you explore a bunch of text in new visual ways. Instead of hunting for confidence levels and p-values, I could now look at my corpus as a two-dimensional map of a landscape.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/0d5ba7de47f13776e18e47d8f830928a30a4fb6e-3199x1379.png?w=800&fit=max&auto=format" alt="Dense network graph of many interconnected words, structured around "History" at the top, "War" on the bottom left, and "Peace" on the bottom right." /><figcaption>The semantic landscape of War and Peace. Rendered by David McClure</figcaption></figure><h3>…and into the multi-dimensional vector space</h3><p>And then Google published <em>word2vec</em>. Introducing the idea that you could translate a corpus into multi-dimensional vectors, and then use the vector distance between the <em>embeddings</em> of two words to find similar relationships.</p><p>The famous example is that this model could predict that if you took the vector distance between <strong>man</strong> and <strong>king</strong>, then going from <strong>woman</strong> would take you close to <strong>queen</strong>. This worked for other patterns like <strong>small→smaller</strong> would match <strong>big→bigger</strong>. Or <strong>Norway→Oslo</strong> would give you <strong>France→Paris</strong>.</p><p>The kicker was that these patterns emerged from just training on the dataset itself. In other words, you could dig up these inherent relationships in language that emerge from language in a way we couldn’t easily do before.</p><p>It gave you a different lens at culture and human communication.</p><figure><img src="https://cdn.sanity.io/images/ndjrels0/production/6f0b0d58da50d30a0abb2ecfcc5bb1084b1641be-648x430.png?w=800&fit=max&auto=format" alt="A Venn diagram showing Norwegian keywords related to "spirituell" (spiritual) and "religiøs" (religious), with distinct and shared terms listed." /><figcaption>A Venn diagram comparing the semantic neighborhoods of “spirituell” (spiritual) and “religiøs” (religious) in Norwegian Wikipedia using word2vec embeddings. The overlap reveals shared mystical and ascetic concepts, while the distinct regions show how “spiritual” clusters with psychological and New Age terminology, and “religious” with institutional and tradition-bound language.</figcaption></figure><p>This is when I burned out. Not because of the research, but the brutal and unforgiving nature of academic institutions.</p><p>I don’t know how the computational linguists who’d spent entire careers counting words felt when embeddings arrived. But I suspect it rhymes with what I’m feeling now.</p><p>Because that was when I left academia and made technology my full-time job.</p><h2>Climbing the strata of abstractions</h2><p>And now I’m here again. With my profession being redefined by many metric tonnes of words cast into multi-dimensional vector space to be coaxed into prediction machines that force a conversation.</p><p>And like my academic days, it feels like we will have to figure out what the new layer of abstractions are.</p><p>Working with AI is a new skill set. And doing so without losing your professional self-worth takes a mindset shift.</p><p>We’re getting new jargon and methodologies. Getting stuff done with AI is easier and less frustrating when you understand what it really is and what the constraints are. Right now, it’s useful to know about context windows and how to work around them. Just as it was useful to really understand memory allocation back in the days (still is in some lines of programming, of course).</p><p>Yes, giving over to vibe-coding will corrode some of your development skills (because you’re not rehearsing them anymore), but it will also give you time to learn new skills, still to be uncovered.</p><p>Yes, using AI to generate content will make some of your writing skills atrophy, but it will also allow you to develop a sharper editorial eye and a deeper understanding of what makes content resonate with humans.</p><p>And what will be forever true: human institutions and innovations will remain messy, predictable, unevenly distributed, creative, and formulaic.</p><p>I’m still in the valley. The view from here is strange. But I’ve hiked out of valleys before.</p><hr /><ol><li id="fn-693c2617be9b"><p>Yes, existential in a fairly narrow sense and certainly from privileged position.</p></li></ol>]]></content:encoded>
      <author>Knut Melvær</author>
    </item>
    <item>
      <title>I set up OpenClaw this morning. By the afternoon it had automated our sauna bookings.</title>
      <link>https://knut.fyi/blog/2026-02-16/openclaw-sauna-automation</link>
      <guid isPermaLink="true">https://knut.fyi/blog/2026-02-16/openclaw-sauna-automation</guid>
      <pubDate>Mon, 16 Feb 2026 02:03:30 GMT</pubDate>
      <description>I installed OpenClaw this morning. By the afternoon, my AI agent had reverse-engineered a booking platform and built a working sauna reservation system. No code written by a human.</description>
      <content:encoded><![CDATA[<p>I&#x27;m late to <a href="https://github.com/openclaw/openclaw">OpenClaw</a>. Everyone in my feed was raving about it two weeks ago and I kept meaning to try it. Then today <a href="https://steipete.com">Peter Steinberger</a> (the creator) <a href="https://www.cnbc.com/2026/02/15/openclaw-creator-peter-steinberger-joining-openai-altman-says.html">joined OpenAI</a> and the whole thing is moving into a foundation, so I figured now&#x27;s the time before I&#x27;m even more behind.</p><p>I installed it this morning. By the afternoon, my household had a working sauna booking system over chat. I didn&#x27;t write a single line of code.</p><p>Here&#x27;s what actually happened.</p><h2>The problem (yes, it involves a sauna)</h2><p>My local sauna spot in Oakland doesn&#x27;t have an API. No developer docs, no OAuth, no webhooks. Just a Rails app with a booking calendar. I book a sauna almost every week, and the flow is always the same: open browser, log in, navigate to the date, scroll for a slot, click, confirm. Not painful, just repetitive.</p><p>I&#x27;m a developer. I could have written a scraper months ago. I didn&#x27;t, because life.</p><h2>What OpenClaw is</h2><p><a href="https://github.com/openclaw/openclaw">OpenClaw</a> runs on your own hardware at home. You give it a personality, connect it to your messaging and services, and let it figure things out.</p><p>I named mine Nisse. In Norwegian folklore, a nisse (or fjøsnisse) is a small, bearded creature that lives in your barn or house. If you treat it well, it looks after the farm, does chores at night, keeps things running. If you forget to leave it a bowl of porridge at Christmas, things start going wrong. Felt right for something that quietly takes care of stuff around the house.</p><p>By mid-morning Nisse had connected to our messaging, set up our calendars, and learned our preferences. Then I pointed it at the sauna booking site and said: figure out how this works and build a way to book from here.</p><h2>What the agent built (with some nudging)</h2><p>About 20 minutes later I had a working MCP server. <a href="https://modelcontextprotocol.io">MCP (Model Context Protocol)</a> is an open standard that lets AI agents use tools through a structured interface. The agent reverse-engineered the booking platform&#x27;s session flow, wrote about 400 lines of JavaScript, and gave itself four tools: check availability, book, list upcoming, cancel.</p><p>I should be honest about the &quot;20 minutes&quot; part. It wasn&#x27;t fully autonomous. It tried really hard to figure out if there was an actual API and gave up. But since the availability calendar was a client side JavaScript dingus, I guessed that there was something going on that implied some structured data somewhere.</p><p>So I had to nudge it once to look more carefully at the DOM for data structures it was missing. That was it. One hint. The agent figured out the rest: the login flow, the session cookies, the event ID encoding, the booking POST.</p><p>The booking platform is <a href="https://floathelm.com">FloatHelm</a>, used by a lot of float spas and wellness centers. No public API. The agent had to:</p><ul><li>Extract CSRF tokens from meta tags and maintain Rails session cookies</li><li>Send XHR headers to get usable responses instead of HTML redirects</li><li>Decode the event ID scheme (room, date, time, duration, and service encoded into one string: <code>1201875-2026-2-21-13-15-45--1213507</code>)</li><li>Parse calendar HTML with <a href="https://cheerio.js.org">Cheerio</a> to extract available slots</li><li>POST reservations with the right &quot;pay in shop&quot; flag for our membership</li></ul><p>None of this was documented anywhere. The agent inspected the site, tried things, and built a working integration. I tested it. It worked. We booked Saturday&#x27;s sauna.</p><h2>What it&#x27;s like now</h2><p>I message &quot;any sauna slots Saturday afternoon?&quot; and get back a list. I pick one, it books it, adds it to our shared calendar. My wife can see it. She can also message the same bot and book or cancel herself. OpenClaw is set up so we can have individual and shared workflows.</p><p>The weekly friction of booking went from &quot;open browser, log in, hunt for a slot, click through&quot; to a single message. Small thing. But small repeated frictions are exactly what automation should target.</p><h2>The trade-offs</h2><p>I want to be straightforward about what this is and isn&#x27;t.</p><p><strong>There&#x27;s no API contract.</strong> I&#x27;m parsing HTML that was designed for browsers, not programs. If FloatHelm redesigned their markup, it breaks. In practice, their interface has been stable for years and the markup is clean, so I&#x27;m not losing sleep over it. But it&#x27;s worth naming. And if/when it breaks, I can probably just ask Nisse to fix it (or it just does it itself).</p><p><strong>Credentials stored locally.</strong> Fine for a personal tool on a personal machine, not a pattern I&#x27;d recommend for anything shared.</p><p><strong>The code is duct tape.</strong> Tightly coupled to specific selectors and form fields. Good enough for a booking or two a week. Not production software. (Not trying to be production software.)</p><h2>Here&#x27;s the thing though</h2><p>It&#x27;s not about saunas. Most of the services we use daily don&#x27;t have APIs. Your dentist, your gym, your local restaurant&#x27;s reservation system. They have websites designed for clicking, not querying. The data is right there on the page, just not structured for programmatic access. Maybe WebMCP will help fix that? </p><p>The gap between &quot;this website has the information I need&quot; and &quot;I can act on it programmatically&quot; used to require a developer sitting down for an afternoon with browser dev tools. Now it requires a 20-minute conversation with your agent.</p><p>I suspect we&#x27;ll see a lot more of this. Not because it&#x27;s elegant (it&#x27;s really not), but because it&#x27;s practical. And the interesting thing about OpenClaw specifically is that the agent doesn&#x27;t just execute tools, it builds them. I described a problem and it came back with a working integration.</p><h2>The morning-to-afternoon thing</h2><p>That&#x27;s the part I keep coming back to. Not that AI can write code (we know). But the full loop: install a framework, connect it to your messaging, point it at an undocumented website, and end up with a working household tool that two people use through a chat interface.</p><p>Setup to genuinely useful, in one day, zero code written by a human. I&#x27;m late to <a href="https://github.com/openclaw/openclaw">OpenClaw</a>, sure. But at least I showed up on the day it made headlines.</p>]]></content:encoded>
      <author>Knut Melvær</author>
    </item>
  </channel>
</rss>