<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: hacker-news</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/hacker-news.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-03-21T23:59:47+00:00</updated><author><name>Simon Willison</name></author><entry><title>Profiling Hacker News users based on their comments</title><link href="https://simonwillison.net/2026/Mar/21/profiling-hacker-news-users/#atom-tag" rel="alternate"/><published>2026-03-21T23:59:47+00:00</published><updated>2026-03-21T23:59:47+00:00</updated><id>https://simonwillison.net/2026/Mar/21/profiling-hacker-news-users/#atom-tag</id><summary type="html">
    &lt;p&gt;Here's a mildly dystopian prompt I've been experimenting with recently: "Profile this user", accompanied by a copy of their last 1,000 comments on Hacker News.&lt;/p&gt;
&lt;p&gt;Obtaining those comments is easy. The &lt;a href="https://hn.algolia.com/api"&gt;Algolia Hacker News API&lt;/a&gt; supports listing comments sorted by date that have a specific tag, and the author of a comment is tagged there as &lt;code&gt;author_username&lt;/code&gt;. Here's a JSON feed of my (&lt;code&gt;simonw&lt;/code&gt;) most recent comments, for example:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://hn.algolia.com/api/v1/search_by_date?tags=comment,author_simonw&amp;amp;hitsPerPage=1000"&gt;https://hn.algolia.com/api/v1/search_by_date?tags=comment,author_simonw&amp;amp;hitsPerPage=1000&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The Algolia API is served with open CORS headers, which means you can access the API from JavaScript running on any web page.&lt;/p&gt;
&lt;p&gt;Last August I &lt;a href="https://chatgpt.com/share/68a4d2b1-a678-8006-aca0-cec51b243bd3"&gt;had ChatGPT build me&lt;/a&gt; a &lt;a href="https://tools.simonwillison.net/hn-comments-for-user"&gt;simple tool&lt;/a&gt; for hitting that API for any user which fetches their comments and gives me a mobile-friendly "copy to clipboard" button. I've since &lt;a href="https://github.com/simonw/tools/commits/main/hn-comments-for-user.html"&gt;tweaked it a few times&lt;/a&gt; with Claude.&lt;/p&gt;
&lt;p&gt;I can then paste the whole lot into any LLM - these days I mostly use Claude Opus 4.6 for this - and prompt "profile this user" to see what it can figure out.&lt;/p&gt;
&lt;p&gt;It's startlingly effective! It feels invasive to quote the profile of another user here, so I'll show you what it produces for me:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is &lt;strong&gt;Simon Willison&lt;/strong&gt; — a prolific, independent software developer, blogger, and one of the most visible voices in the AI-assisted coding space. Here's a profile drawn from his comments:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Professional identity:&lt;/strong&gt; Independent developer and writer. Co-creator of Django, creator of Datasette and many other open source tools. On the board of the Python Software Foundation. Runs a heavily-trafficked personal blog at simonwillison.net with multiple content types (entries, link blog, notes, quotations). Monetizes through GitHub sponsors, ethical ads, and occasional consulting — not through any AI company payroll, a point he defends frequently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core thesis on AI coding:&lt;/strong&gt; He's the leading evangelist for what he calls "agentic engineering" — using coding agents (Claude Code, Codex CLI) as productivity multipliers for skilled developers. His central argument: LLMs don't replace programmers, they amplify existing expertise. He's writing a guide/book on this at simonwillison.net/guides. He insists these tools are deceptively difficult to use well, and that most people who report bad results simply haven't learned the craft yet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Working style:&lt;/strong&gt; He does most of his programming from his iPhone via Claude Code for web, often while riding BART, walking the dog, or sitting in the garden. He embraces "YOLO mode" (auto-approving agent actions) and frequently runs 2-3 agent sessions in parallel. He starts every session with "run uv run pytest" to anchor the agent in TDD. He's on the $200/month Claude Max plan.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key technical interests:&lt;/strong&gt; Sandboxing and security (WebAssembly, Pyodide, sandbox-exec, Firecracker), SQLite (deeply), Python packaging and tooling (uv, PyPI distribution tricks), browser-in-a-browser experiments (v86, WASM Linux), and local LLM inference. He's been learning Go "by osmosis" through coding agents.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security consciousness:&lt;/strong&gt; He coined the term "prompt injection" and the "lethal trifecta" (access to private data + exposure to untrusted input + ability to take actions). He's genuinely alarmed about OpenClaw/Clawdbot security risks and has been predicting a headline-grabbing prompt injection attack "in the next six months" for over two years — acknowledging it keeps not happening while insisting the risk is real. He references the "normalization of deviance" pattern frequently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Personality and debate style:&lt;/strong&gt; Energetic, combative when challenged, but generally good-natured. He engages heavily in HN comment threads — sometimes dozens of replies in a single discussion. He's transparent about his biases and maintains a public disclosures page. He pushes back hard against what he sees as motivated reasoning from AI skeptics, but also against AGI hype ("I refer to that as the science fiction version of AI"). He gets visibly frustrated by sock-puppet accounts and bad-faith criticism. He has a self-deprecating streak ("my stupid pelican benchmark").&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recurring themes across hundreds of comments:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"Two things can be true at the same time" — he holds nuanced positions&lt;/li&gt;
&lt;li&gt;Tests are for productivity, not just quality&lt;/li&gt;
&lt;li&gt;The November 2025 model releases (Opus 4.5, GPT-5.2) were a genuine inflection point&lt;/li&gt;
&lt;li&gt;Code review is the biggest bottleneck in agent-assisted workflows&lt;/li&gt;
&lt;li&gt;"Cognitive debt" is a real and unsolved problem&lt;/li&gt;
&lt;li&gt;The best engineering practices (tests, docs, CI/CD, clean code) also make agents work better&lt;/li&gt;
&lt;li&gt;He's deliberately trying to "teach people good software engineering while tricking them into thinking the book is about AI"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Personal interests mentioned:&lt;/strong&gt; Niche museums (runs niche-museums.com), New Zealand kākāpō parrots, cooking food from countries he's never visited, chickens in his garden. Lives in Half Moon Bay, CA near a major pelican roost. The pelican-on-a-bicycle SVG test is his signature LLM benchmark.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In a sentence:&lt;/strong&gt; A deeply experienced, independently-minded developer who's genuinely excited about AI coding tools, fights hard against both uncritical hype and reflexive dismissal, and is trying to professionalize how the industry uses these tools — while also worrying loudly about the security implications almost nobody else takes seriously enough.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This all checks out! I ran this in Claude incognito mode to hopefully prevent Claude from guessing that I was evaluating myself and sycophantically glazing me - the tone of the response it gave here is similar to the tone I've seen against other accounts.&lt;/p&gt;

&lt;p&gt;I expect it guessed my real name due to my habit of linking to my own writing from some of my comments, which provides plenty of simonwillison.net URLs for it to associate with my public persona. I haven't seen it take a guess at a real name for any of the other profiles I've generated.&lt;/p&gt;
&lt;p&gt;It's a little creepy to be able to derive this much information about someone so easily, even when they've shared that freely in a public (and API-available) place.&lt;/p&gt;
&lt;p&gt;I mainly use this to check that I'm not getting embroiled in an extensive argument with someone who has a history of arguing in bad faith. Thankfully that's rarely the case - Hacker News continues to be a responsibly moderated online space.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-ethics"&gt;ai-ethics&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="hacker-news"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-ethics"/></entry><entry><title>Tips for getting coding agents to write good Python tests</title><link href="https://simonwillison.net/2026/Jan/26/tests/#atom-tag" rel="alternate"/><published>2026-01-26T23:55:29+00:00</published><updated>2026-01-26T23:55:29+00:00</updated><id>https://simonwillison.net/2026/Jan/26/tests/#atom-tag</id><summary type="html">
    &lt;p&gt;Someone &lt;a href="https://news.ycombinator.com/item?id=46765460#46765823"&gt;asked&lt;/a&gt; on Hacker News if I had any tips for getting coding agents to write decent quality tests. Here's what I said:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I work in Python which helps a lot because there are a TON of good examples of pytest tests floating around in the training data, including things like usage of fixture libraries for mocking external HTTP APIs and snapshot testing and other neat patterns.&lt;/p&gt;
&lt;p&gt;Or I can say "use pytest-httpx to mock the endpoints" and Claude knows what I mean.&lt;/p&gt;
&lt;p&gt;Keeping an eye on the tests is important. The most common anti-pattern I see is large amounts of duplicated test setup code - which isn't a huge deal, I'm much more more tolerant of duplicated logic in tests than I am in implementation, but it's still worth pushing back on.&lt;/p&gt;
&lt;p&gt;"Refactor those tests to use pytest.mark.parametrize" and "extract the common setup into a pytest fixture" work really well there.&lt;/p&gt;
&lt;p&gt;Generally though the best way to get good tests out of a coding agent is to make sure it's working in a project with an existing test suite that uses good patterns. Coding agents pick the existing patterns up without needing any extra prompting at all.&lt;/p&gt;
&lt;p&gt;I find that once a project has clean basic tests the new tests added by the agents tend to match them in quality. It's similar to how working on large projects with a team of other developers work - keeping the code clean means when people look for examples of how to write a test they'll be pointed in the right direction.&lt;/p&gt;
&lt;p&gt;One last tip I use a lot is this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Clone datasette/datasette-enrichments
from GitHub to /tmp and imitate the
testing patterns it uses
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I do this all the time with different existing projects I've written - the quickest way to show an agent how you like something to be done is to have it look at an example.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="python"/><category term="testing"/><category term="ai"/><category term="pytest"/><category term="generative-ai"/><category term="llms"/><category term="coding-agents"/></entry><entry><title>The most popular blogs of Hacker News in 2025</title><link href="https://simonwillison.net/2026/Jan/2/most-popular-blogs-of-hacker-news/#atom-tag" rel="alternate"/><published>2026-01-02T19:10:43+00:00</published><updated>2026-01-02T19:10:43+00:00</updated><id>https://simonwillison.net/2026/Jan/2/most-popular-blogs-of-hacker-news/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://refactoringenglish.com/blog/2025-hn-top-5/"&gt;The most popular blogs of Hacker News in 2025&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Michael Lynch maintains &lt;a href="https://refactoringenglish.com/tools/hn-popularity/"&gt;HN Popularity Contest&lt;/a&gt;, a site that tracks personal blogs on Hacker News and scores them based on how well they perform on that platform.&lt;/p&gt;
&lt;p&gt;The engine behind the project is the &lt;a href="https://github.com/mtlynch/hn-popularity-contest-data/blob/master/data/domains-meta.csv"&gt;domain-meta.csv&lt;/a&gt; CSV on GiHub, a hand-curated list of known personal blogs with author and bio and tag metadata, which Michael uses to separate out personal blog posts from other types of content.&lt;/p&gt;
&lt;p&gt;I came top of the rankings in 2023, 2024 and 2025 but I'm listed &lt;a href="https://refactoringenglish.com/tools/hn-popularity/"&gt;in third place&lt;/a&gt; for all time behind Paul Graham and Brian Krebs.&lt;/p&gt;
&lt;p&gt;I dug around in the browser inspector and was delighted to find that the data powering the site is served with open CORS headers, which means you can easily explore it with external services like Datasette Lite.&lt;/p&gt;
&lt;p&gt;Here's a convoluted window function query Claude Opus 4.5 &lt;a href="https://claude.ai/share/8e1cb294-0ff0-4d5b-b83f-58e4c7fdb0d2"&gt;wrote for me&lt;/a&gt; which, for a given domain, shows where that domain ranked for each year since it first appeared in the dataset:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s"&gt;with yearly_scores as (&lt;/span&gt;
&lt;span class="pl-s"&gt;  select &lt;/span&gt;
&lt;span class="pl-s"&gt;    domain,&lt;/span&gt;
&lt;span class="pl-s"&gt;    strftime('%Y', date) as year,&lt;/span&gt;
&lt;span class="pl-s"&gt;    sum(score) as total_score,&lt;/span&gt;
&lt;span class="pl-s"&gt;    count(distinct date) as days_mentioned&lt;/span&gt;
&lt;span class="pl-s"&gt;  from "hn-data"&lt;/span&gt;
&lt;span class="pl-s"&gt;  group by domain, strftime('%Y', date)&lt;/span&gt;
&lt;span class="pl-s"&gt;),&lt;/span&gt;
&lt;span class="pl-s"&gt;ranked as (&lt;/span&gt;
&lt;span class="pl-s"&gt;  select &lt;/span&gt;
&lt;span class="pl-s"&gt;    domain,&lt;/span&gt;
&lt;span class="pl-s"&gt;    year,&lt;/span&gt;
&lt;span class="pl-s"&gt;    total_score,&lt;/span&gt;
&lt;span class="pl-s"&gt;    days_mentioned,&lt;/span&gt;
&lt;span class="pl-s"&gt;    rank() over (partition by year order by total_score desc) as rank&lt;/span&gt;
&lt;span class="pl-s"&gt;  from yearly_scores&lt;/span&gt;
&lt;span class="pl-s"&gt;)&lt;/span&gt;
&lt;span class="pl-s"&gt;select &lt;/span&gt;
&lt;span class="pl-s"&gt;  r.year,&lt;/span&gt;
&lt;span class="pl-s"&gt;  r.total_score,&lt;/span&gt;
&lt;span class="pl-s"&gt;  r.rank,&lt;/span&gt;
&lt;span class="pl-s"&gt;  r.days_mentioned&lt;/span&gt;
&lt;span class="pl-s"&gt;from ranked r&lt;/span&gt;
&lt;span class="pl-s"&gt;where r.domain = :domain&lt;/span&gt;
&lt;span class="pl-s"&gt;  and r.year &amp;gt;= (&lt;/span&gt;
&lt;span class="pl-s"&gt;    select min(strftime('%Y', date)) &lt;/span&gt;
&lt;span class="pl-s"&gt;    from "hn-data"&lt;/span&gt;
&lt;span class="pl-s"&gt;    where domain = :domain&lt;/span&gt;
&lt;span class="pl-s"&gt;  )&lt;/span&gt;
&lt;span class="pl-s"&gt;order by r.year desc&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;(I just noticed that the last &lt;code&gt;and r.year &amp;gt;= (&lt;/code&gt; clause isn't actually needed here.)&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://lite.datasette.io/?csv=https://hn-popularity.cdn.refactoringenglish.com/hn-data.csv#/data?sql=with+yearly_scores+as+%28%0A++select+%0A++++domain%2C%0A++++strftime%28%27%25Y%27%2C+date%29+as+year%2C%0A++++sum%28score%29+as+total_score%2C%0A++++count%28distinct+date%29+as+days_mentioned%0A++from+%22hn-data%22%0A++group+by+domain%2C+strftime%28%27%25Y%27%2C+date%29%0A%29%2C%0Aranked+as+%28%0A++select+%0A++++domain%2C%0A++++year%2C%0A++++total_score%2C%0A++++days_mentioned%2C%0A++++rank%28%29+over+%28partition+by+year+order+by+total_score+desc%29+as+rank%0A++from+yearly_scores%0A%29%0Aselect+%0A++r.year%2C%0A++r.total_score%2C%0A++r.rank%2C%0A++r.days_mentioned%0Afrom+ranked+r%0Awhere+r.domain+%3D+%3Adomain%0A++and+r.year+%3E%3D+%28%0A++++select+min%28strftime%28%27%25Y%27%2C+date%29%29+%0A++++from+%22hn-data%22%0A++++where+domain+%3D+%3Adomain%0A++%29%0Aorder+by+r.year+desc&amp;amp;domain=simonwillison.net"&gt;simonwillison.net results&lt;/a&gt; show me ranked 3rd in 2022, 30th in 2021 and 85th back in 2007 - though I expect there are many personal blogs from that year which haven't yet been manually added to Michael's list.&lt;/p&gt;
&lt;p&gt;Also useful is that every domain gets its own CORS-enabled CSV file with details of the actual Hacker News submitted from that domain, e.g. &lt;code&gt;https://hn-popularity.cdn.refactoringenglish.com/domains/simonwillison.net.csv&lt;/code&gt;. Here's &lt;a href="https://lite.datasette.io/?csv=https://hn-popularity.cdn.refactoringenglish.com/domains/simonwillison.net.csv#/data/simonwillison"&gt;that one in Datasette Lite&lt;/a&gt;.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=46465819"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-lite"&gt;datasette-lite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cors"&gt;cors&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="sql"/><category term="sqlite"/><category term="datasette"/><category term="datasette-lite"/><category term="cors"/></entry><entry><title>Could LLMs encourage new programming languages?</title><link href="https://simonwillison.net/2025/Nov/7/llms-for-new-programming-languages/#atom-tag" rel="alternate"/><published>2025-11-07T16:00:42+00:00</published><updated>2025-11-07T16:00:42+00:00</updated><id>https://simonwillison.net/2025/Nov/7/llms-for-new-programming-languages/#atom-tag</id><summary type="html">
    &lt;p&gt;My hunch is that existing LLMs make it &lt;em&gt;easier&lt;/em&gt; to build a new programming language in a way that captures new developers.&lt;/p&gt;
&lt;p&gt;Most programming languages are similar enough to existing languages that you only need to know a small number of details to use them: what's the core syntax for variables, loops, conditionals and functions? How does memory management work? What's the concurrency model?&lt;/p&gt;
&lt;p&gt;For many languages you can fit all of that, including illustrative examples, in a few thousand tokens of text.&lt;/p&gt;
&lt;p&gt;So ship your new programming language with a &lt;a href="https://simonwillison.net/2025/Oct/16/claude-skills/"&gt;Claude Skills style document&lt;/a&gt; and give your early adopters the ability to write it with LLMs. The LLMs should handle that very well, especially if they get to run an agentic loop against a compiler or even a linter that you provide.&lt;/p&gt;
&lt;p&gt;&lt;small&gt;This post started &lt;a href="https://news.ycombinator.com/context?id=45847505"&gt;as a comment&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/programming-languages"&gt;programming-languages&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/skills"&gt;skills&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="programming-languages"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="coding-agents"/><category term="skills"/></entry><entry><title>Setting up a codebase for working with coding agents</title><link href="https://simonwillison.net/2025/Oct/25/coding-agent-tips/#atom-tag" rel="alternate"/><published>2025-10-25T18:42:24+00:00</published><updated>2025-10-25T18:42:24+00:00</updated><id>https://simonwillison.net/2025/Oct/25/coding-agent-tips/#atom-tag</id><summary type="html">
    &lt;p&gt;Someone on Hacker News &lt;a href="https://news.ycombinator.com/item?id=45695621#45704966"&gt;asked for tips&lt;/a&gt; on setting up a codebase to be more productive with AI coding tools. Here's my reply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Good automated tests which the coding agent can run. I love pytest for this - one of my projects has 1500 tests and Claude Code is really good at selectively executing just tests relevant to the change it is making, and then running the whole suite at the end.&lt;/li&gt;
&lt;li&gt;Give them the ability to interactively test the code they are writing too. Notes on how to start a development server (for web projects) are useful, then you can have them use Playwright or curl to try things out.&lt;/li&gt;
&lt;li&gt;I'm having great results from maintaining a GitHub issues collection for projects and pasting URLs to issues directly into Claude Code.&lt;/li&gt;
&lt;li&gt;I actually don't think documentation is too important: LLMs can read the code a lot faster than you to figure out how to use it. I have comprehensive documentation across all of my projects but I don't think it's that helpful for the coding agents, though they are good at helping me spot if it needs updating.&lt;/li&gt;
&lt;li&gt;Linters, type checkers, auto-formatters - give coding agents helpful tools to run and they'll use them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the most part anything that makes a codebase easier for humans to maintain turns out to help agents as well.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Thought of another one: detailed error messages! If a manual or automated test fails the more information you can return back to the model the better, and stuffing extra data in the error message or assertion is a very inexpensive way to do that.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="ai"/><category term="pytest"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="coding-agents"/></entry><entry><title>Quoting IanCal</title><link href="https://simonwillison.net/2025/Sep/6/iancal/#atom-tag" rel="alternate"/><published>2025-09-06T06:41:49+00:00</published><updated>2025-09-06T06:41:49+00:00</updated><id>https://simonwillison.net/2025/Sep/6/iancal/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://news.ycombinator.com/item?id=45135302#45135852"&gt;&lt;p&gt;RDF has the same problems as the SQL schemas with information scattered. What fields mean requires documentation.&lt;/p&gt;
&lt;p&gt;There - they have a name on a person. What name? Given? Legal? Chosen? Preferred for this use case?&lt;/p&gt;
&lt;p&gt;You only have one ID for Apple eh? Companies are complex to model, do you mean Apple just as someone would talk about it? The legal structure of entities that underpins all major companies, what part of it is referred to?&lt;/p&gt;
&lt;p&gt;I spent a long time building identifiers for universities and companies (which was taken for &lt;a href="https://ror.org/"&gt;ROR&lt;/a&gt; later) and it was a nightmare to say what a university even was. What’s the name of Cambridge? It’s not “Cambridge University” or “The university of Cambridge” legally. But it also is the actual name as people use it. &lt;em&gt;[It's &lt;a href="https://www.cam.ac.uk/about-the-university/how-the-university-and-colleges-work/the-university-as-a-charity"&gt;The Chancellor, Masters, and Scholars of the University of Cambridge&lt;/a&gt;]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The university of Paris went from something like 13 institutes to maybe one to then a bunch more. Are companies locations at their headquarters? Which headquarters?&lt;/p&gt;
&lt;p&gt;Someone will suggest modelling to solve this but here lies the biggest problem:&lt;/p&gt;
&lt;p&gt;The correct modelling depends on &lt;em&gt;the questions you want to answer&lt;/em&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://news.ycombinator.com/item?id=45135302#45135852"&gt;IanCal&lt;/a&gt;, on Hacker News, discussing RDF&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/metadata"&gt;metadata&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rdf"&gt;rdf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="metadata"/><category term="rdf"/><category term="sql"/></entry><entry><title>Quoting potatolicious</title><link href="https://simonwillison.net/2025/Aug/21/potatolicious/#atom-tag" rel="alternate"/><published>2025-08-21T21:44:19+00:00</published><updated>2025-08-21T21:44:19+00:00</updated><id>https://simonwillison.net/2025/Aug/21/potatolicious/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://news.ycombinator.com/item?id=44976929#44978319"&gt;&lt;p&gt;Most classical engineering fields deal with probabilistic system components all of the time. In fact I'd go as far as to say that &lt;em&gt;inability&lt;/em&gt; to deal with probabilistic components is disqualifying from many engineering endeavors.&lt;/p&gt;
&lt;p&gt;Process engineers for example have to account for human error rates. On a given production line with humans in a loop, the operators will sometimes screw up. Designing systems to detect these errors (which are &lt;em&gt;highly probabilistic&lt;/em&gt;!), mitigate them, and reduce the occurrence rates of such errors is a huge part of the job. [...]&lt;/p&gt;
&lt;p&gt;Software engineering is &lt;em&gt;unlike&lt;/em&gt; traditional engineering disciplines in that for most of its lifetime it's had the luxury of purely deterministic expectations. This is not true in nearly every other type of engineering.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://news.ycombinator.com/item?id=44976929#44978319"&gt;potatolicious&lt;/a&gt;, in a conversation about AI engineering&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/software-engineering"&gt;software-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="software-engineering"/><category term="ai"/><category term="generative-ai"/></entry><entry><title>Quoting mrmincent</title><link href="https://simonwillison.net/2025/Jul/1/mrmincent/#atom-tag" rel="alternate"/><published>2025-07-01T17:07:27+00:00</published><updated>2025-07-01T17:07:27+00:00</updated><id>https://simonwillison.net/2025/Jul/1/mrmincent/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://news.ycombinator.com/item?id=44429225#44431095"&gt;&lt;p&gt;To misuse a woodworking metaphor, I think we’re experiencing a shift from hand tools to power tools.&lt;/p&gt;
&lt;p&gt;You still need someone who understands the basics to get the good results out of the tools, but they’re not chiseling fine furniture by hand anymore, they’re throwing heaps of wood through the tablesaw instead. More productive, but more likely to lose a finger if you’re not careful.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://news.ycombinator.com/item?id=44429225#44431095"&gt;mrmincent&lt;/a&gt;, Hacker News comment on Claude Code&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude-code"/></entry><entry><title>My AI Skeptic Friends Are All Nuts</title><link href="https://simonwillison.net/2025/Jun/2/my-ai-skeptic-friends-are-all-nuts/#atom-tag" rel="alternate"/><published>2025-06-02T23:56:49+00:00</published><updated>2025-06-02T23:56:49+00:00</updated><id>https://simonwillison.net/2025/Jun/2/my-ai-skeptic-friends-are-all-nuts/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://fly.io/blog/youre-all-nuts/"&gt;My AI Skeptic Friends Are All Nuts&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Thomas Ptacek's frustrated tone throughout this piece perfectly captures how it feels sometimes to be an experienced programmer trying to argue that "LLMs are actually really useful" in many corners of the internet.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Some of the smartest people I know share a bone-deep belief that AI is a fad — the next iteration of NFT mania. I’ve been reluctant to push back on them, because, well, they’re smarter than me. But their arguments are unserious, and worth confronting. Extraordinarily talented people are doing work that LLMs already do better, out of spite. [...]&lt;/p&gt;
&lt;p&gt;You’ve always been responsible for what you merge to &lt;code&gt;main&lt;/code&gt;. You were five years go. And you are tomorrow, whether or not you use an LLM. [...]&lt;/p&gt;
&lt;p&gt;Reading other people’s code is part of the job. If you can’t metabolize the boring, repetitive code an LLM generates: skills issue! How are you handling the chaos human developers turn out on a deadline?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And on the threat of AI taking jobs from engineers (with a link to an old comment of mine):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://news.ycombinator.com/item?id=43775358#43776612"&gt;So does open source.&lt;/a&gt; We used to pay good money for databases.&lt;/p&gt;
&lt;p&gt;We're a field premised on automating other people's jobs away. "Productivity gains," say the economists. You get what that means, right? Fewer people doing the same stuff. Talked to a travel agent lately? Or a floor broker? Or a record store clerk? Or a darkroom tech?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The post has already attracted &lt;a href="https://news.ycombinator.com/item?id=44163063"&gt;695 comments&lt;/a&gt; on Hacker News in just two hours, which feels like some kind of record even by the usual standards of fights about AI on the internet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Thomas, another hundred or so comments &lt;a href="https://news.ycombinator.com/item?id=44163063#44165137"&gt;later&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A lot of people are misunderstanding the goal of the post, which is not necessarily to persuade them, but rather to disrupt a static, unproductive equilibrium of uninformed arguments about how this stuff works. The commentary I've read today has to my mind vindicated that premise.&lt;/p&gt;
&lt;/blockquote&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://bsky.app/profile/sockpuppet.org/post/3lqnoo5irzs2b"&gt;@sockpuppet.org&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/thomas-ptacek"&gt;thomas-ptacek&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="thomas-ptacek"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/></entry><entry><title>llm-hacker-news 0.1.1</title><link href="https://simonwillison.net/2025/May/5/llm-hacker-news/#atom-tag" rel="alternate"/><published>2025-05-05T17:10:45+00:00</published><updated>2025-05-05T17:10:45+00:00</updated><id>https://simonwillison.net/2025/May/5/llm-hacker-news/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/llm-hacker-news/releases/tag/0.1.1"&gt;llm-hacker-news 0.1.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="hacker-news"/><category term="llm"/></entry><entry><title>The GeoGuessr StreetView meta-game</title><link href="https://simonwillison.net/2025/Apr/26/geoguessr/#atom-tag" rel="alternate"/><published>2025-04-26T16:56:59+00:00</published><updated>2025-04-26T16:56:59+00:00</updated><id>https://simonwillison.net/2025/Apr/26/geoguessr/#atom-tag</id><summary type="html">
    &lt;p&gt;My post on &lt;a href="https://simonwillison.net/2025/Apr/26/o3-photo-locations/"&gt;o3 guessing locations from photos&lt;/a&gt; made it &lt;a href="https://news.ycombinator.com/item?id=43803243"&gt;to Hacker News&lt;/a&gt; and by far the most interesting comments are from SamPatt, a self-described competitive &lt;a href="https://www.geoguessr.com/"&gt;GeoGuessr&lt;/a&gt; player.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://news.ycombinator.com/item?id=43803243#43804551"&gt;a thread&lt;/a&gt; about meta-knowledge of the StreetView card uses in different regions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The photography matters a great deal - they're categorized into "Generations" of coverage. Gen 2 is low resolution, Gen 3 is pretty good but has a distinct car blur, Gen 4 is highest quality. Each country tends to have only one or two categories of coverage, and some are so distinct you can immediately know a location based solely on that (India is the best example here). [...]&lt;/p&gt;
&lt;p&gt;Nigeria and Tunisia have follow cars. Senegal, Montenegro and Albania have large rifts in the sky where the panorama stitching software did a poor job. Some parts of Russia had recent forest fires and are very smokey. One road in Turkey is in absurdly thick fog. The list is endless, which is why it's so fun!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sam also has &lt;a href="https://news.ycombinator.com/item?id=43803243#43804197"&gt;his own custom Obsidian flashcard deck&lt;/a&gt; "with hundreds of entries to help me remember road lines, power poles, bollards, architecture, license plates, etc".&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://news.ycombinator.com/item?id=43805123"&gt;asked Sam&lt;/a&gt; how closely the GeoGuessr community track updates to street view imagery, and unsurprisingly those are a &lt;em&gt;big&lt;/em&gt; deal. Sam pointed me to &lt;a href="https://www.youtube.com/watch?v=XLETln6ZatE"&gt;this 10 minute video review&lt;/a&gt; by zi8gzag of the latest big update from three weeks ago:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is one of the biggest updates in years in my opinion. It could be the biggest update since the 2022 update that gave Gen 4 to Nigeria, Senegal, and Rwanda. It's definitely on the same level as the Kazakhstan update or the Germany update in my opinion.&lt;/p&gt;
&lt;/blockquote&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/streetview"&gt;streetview&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geoguessing"&gt;geoguessing&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="hacker-news"/><category term="streetview"/><category term="geoguessing"/></entry><entry><title>llm-hacker-news</title><link href="https://simonwillison.net/2025/Apr/8/llm-hacker-news/#atom-tag" rel="alternate"/><published>2025-04-08T00:11:30+00:00</published><updated>2025-04-08T00:11:30+00:00</updated><id>https://simonwillison.net/2025/Apr/8/llm-hacker-news/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-hacker-news"&gt;llm-hacker-news&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I built this new plugin to exercise the new &lt;a href="https://llm.datasette.io/en/stable/plugins/plugin-hooks.html#register-fragment-loaders-register"&gt;register_fragment_loaders()&lt;/a&gt; plugin hook I added to &lt;a href="https://simonwillison.net/2025/Apr/7/long-context-llm/"&gt;LLM 0.24&lt;/a&gt;. It's the plugin equivalent of &lt;a href="https://til.simonwillison.net/llms/claude-hacker-news-themes"&gt;the Bash script&lt;/a&gt; I've been using to summarize &lt;a href="https://news.ycombinator.com/"&gt;Hacker News&lt;/a&gt; conversations for the past 18 months.&lt;/p&gt;
&lt;p&gt;You can use it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm install llm-hacker-news
llm -f hn:43615912 'summary with illustrative direct quotes'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see the output &lt;a href="https://github.com/simonw/llm-hacker-news/issues/1#issuecomment-2784887743"&gt;in this issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The plugin registers a &lt;code&gt;hn:&lt;/code&gt; prefix - combine that with the ID of a Hacker News conversation to pull that conversation into the context.&lt;/p&gt;
&lt;p&gt;It uses the Algolia Hacker News API which returns &lt;a href="https://hn.algolia.com/api/v1/items/43615912"&gt;JSON like this&lt;/a&gt;. Rather than feed the JSON directly to the LLM it instead converts it to a hopefully more LLM-friendly format that looks like this example from &lt;a href="https://github.com/simonw/llm-hacker-news/blob/0.1/tests/test_hacker_news.py#L5-L18"&gt;the plugin's test&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[1] BeakMaster: Fish Spotting Techniques

[1.1] CoastalFlyer: The dive technique works best when hunting in shallow waters.

[1.1.1] PouchBill: Agreed. Have you tried the hover method near the pier?

[1.1.2] WingSpan22: My bill gets too wet with that approach.

[1.1.2.1] CoastalFlyer: Try tilting at a 40° angle like our Australian cousins.

[1.2] BrownFeathers: Anyone spotted those "silver fish" near the rocks?

[1.2.1] GulfGlider: Yes! They're best caught at dawn.
Just remember: swoop &amp;gt; grab &amp;gt; lift
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That format was suggested by Claude, which then wrote most of the plugin implementation for me. Here's &lt;a href="https://claude.ai/share/6da6ec5a-b8b3-4572-ab1b-141bb37ef70b"&gt;that Claude transcript&lt;/a&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="plugins"/><category term="projects"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="anthropic"/><category term="claude"/></entry><entry><title>llm-hacker-news 0.1</title><link href="https://simonwillison.net/2025/Apr/7/llm-hacker-news/#atom-tag" rel="alternate"/><published>2025-04-07T23:59:49+00:00</published><updated>2025-04-07T23:59:49+00:00</updated><id>https://simonwillison.net/2025/Apr/7/llm-hacker-news/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/llm-hacker-news/releases/tag/0.1"&gt;llm-hacker-news 0.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="hacker-news"/><category term="llm"/></entry><entry><title>A professional workflow for translation using LLMs</title><link href="https://simonwillison.net/2025/Feb/2/workflow-for-translation/#atom-tag" rel="alternate"/><published>2025-02-02T04:23:19+00:00</published><updated>2025-02-02T04:23:19+00:00</updated><id>https://simonwillison.net/2025/Feb/2/workflow-for-translation/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://news.ycombinator.com/item?id=42897856"&gt;A professional workflow for translation using LLMs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Tom Gally is a &lt;a href="https://gally.net/translation.html"&gt;professional translator&lt;/a&gt; who has been exploring the use of LLMs since the release of GPT-4. In this Hacker News comment he shares a detailed workflow for how he uses them to assist in that process.&lt;/p&gt;
&lt;p&gt;Tom starts with the source text and custom instructions, including context for how the translation will be used. &lt;a href="https://www.gally.net/temp/20250201sampletranslationprompt.html"&gt;Here's an imaginary example prompt&lt;/a&gt;, which starts:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;The text below in Japanese is a product launch presentation for Sony's new gaming console, to be delivered by the CEO at Tokyo Game Show 2025. Please translate it into English. Your translation will be used in the official press kit and live interpretation feed. When translating this presentation, please follow these guidelines to create an accurate and engaging English version that preserves both the meaning and energy of the original: [...]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It then lists some tone, style and content guidelines custom to that text.&lt;/p&gt;
&lt;p&gt;Tom runs that prompt through several different LLMs and starts by picking sentences and paragraphs from those that form a good basis for the translation.&lt;/p&gt;
&lt;p&gt;As he works on the full translation he uses Claude to help brainstorm alternatives for tricky sentences:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When I am unable to think of a good English version for a particular sentence, I give the Japanese and English versions of the paragraph it is contained in to an LLM (usually, these days, Claude) and ask for ten suggestions for translations of the problematic sentence. Usually one or two of the suggestions work fine; if not, I ask for ten more. (Using an LLM as a sentence-level thesaurus on steroids is particularly wonderful.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He uses another LLM and prompt to check his translation against the original and provide further suggestions, which he occasionally acts on. Then as a final step he runs the finished document through a text-to-speech engine to try and catch any "minor awkwardnesses" in the result.&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;love&lt;/em&gt; this as an example of an expert using LLMs as tools to help further elevate their work. I'd love to read more examples &lt;a href="https://news.ycombinator.com/item?id=42897856"&gt;like this one&lt;/a&gt; from experts in other fields.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/translation"&gt;translation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tom-gally"&gt;tom-gally&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="translation"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="tom-gally"/></entry><entry><title>Hacker News conversation on feature flags</title><link href="https://simonwillison.net/2025/Feb/2/feature-flags/#atom-tag" rel="alternate"/><published>2025-02-02T01:18:44+00:00</published><updated>2025-02-02T01:18:44+00:00</updated><id>https://simonwillison.net/2025/Feb/2/feature-flags/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://news.ycombinator.com/item?id=42899778#42900221"&gt;Hacker News conversation on feature flags&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I posted the following comment in a thread on Hacker News about feature flags, in response to this article &lt;a href="https://code.mendhak.com/hardcode-feature-flags/"&gt;It’s OK to hardcode feature flags&lt;/a&gt;. This kicked off a &lt;em&gt;very&lt;/em&gt; high quality conversation on build-vs-buy and running feature flags at scale involving a bunch of very experienced and knowledgeable people. I recommend reading the comments.&lt;/p&gt;
&lt;p&gt;Here's what I said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The single biggest value add of feature flags is that they de-risk deployment. They make it less frightening and difficult to turn features on and off, which means you'll do it more often. This means you can build more confidently and learn faster from what you build. That's worth a lot.&lt;/p&gt;
&lt;p&gt;I think there's a reasonable middle ground-point between having feature flags in a JSON file that you have to redeploy to change and using an (often expensive) feature flags as a service platform: roll your own simple system.&lt;/p&gt;
&lt;p&gt;A relational database lookup against primary keys in a table with a dozen records is effectively free. Heck, load the entire collection at the start of each request - through a short lived cache if your profiling says that would help.&lt;/p&gt;
&lt;p&gt;Once you start getting more complicated (flags enabled for specific users etc) you should consider build-vs-buy more seriously, but for the most basic version you really can have no-deploy-changes at minimal cost with minimal effort.&lt;/p&gt;
&lt;p&gt;There are probably good open source libraries you can use here too, though I haven't gone looking for any in the last five years.&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/feature-flags"&gt;feature-flags&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="feature-flags"/></entry><entry><title>Ask HN: What happens to ".io" TLD after UK gives back the Chagos Islands?</title><link href="https://simonwillison.net/2024/Oct/3/what-happens-to-io-after-uk-gives-back-chagos/#atom-tag" rel="alternate"/><published>2024-10-03T17:25:21+00:00</published><updated>2024-10-03T17:25:21+00:00</updated><id>https://simonwillison.net/2024/Oct/3/what-happens-to-io-after-uk-gives-back-chagos/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://news.ycombinator.com/item?id=41729526"&gt;Ask HN: What happens to &amp;quot;.io&amp;quot; TLD after UK gives back the Chagos Islands?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This morning on the BBC: &lt;a href="https://www.bbc.com/news/articles/c98ynejg4l5o"&gt;UK will give sovereignty of Chagos Islands to Mauritius&lt;/a&gt;. The Chagos Islands include the area that the UK calls &lt;a href="https://en.wikipedia.org/wiki/British_Indian_Ocean_Territory"&gt;the British Indian Ocean Territory&lt;/a&gt;. The &lt;a href="https://en.wikipedia.org/wiki/.io"&gt;.io ccTLD&lt;/a&gt; uses the  ISO-3166 two-letter country code for that designation.&lt;/p&gt;
&lt;p&gt;As the owner of &lt;a href="https://datasette.io/"&gt;datasette.io&lt;/a&gt; the question of what happens to that ccTLD is suddenly very relevant to me.&lt;/p&gt;
&lt;p&gt;This Hacker News conversation has some useful information. It sounds like there's a very real possibility that &lt;code&gt;.io&lt;/code&gt; could be deleted after a few years notice - it's happened before, for ccTLDs such as &lt;code&gt;.zr&lt;/code&gt; for Zaire (which renamed to &lt;a href="https://en.wikipedia.org/wiki/Democratic_Republic_of_the_Congo"&gt;Democratic Republic of the Congo&lt;/a&gt; in 1997, with &lt;code&gt;.zr&lt;/code&gt; withdrawn in 2001) and &lt;a href="https://en.wikipedia.org/wiki/.cs"&gt;.cs&lt;/a&gt; for Czechoslovakia, withdrawn in 1995.&lt;/p&gt;
&lt;p&gt;Could &lt;code&gt;.io&lt;/code&gt; change status to the same kind of TLD as &lt;code&gt;.museum&lt;/code&gt;, unaffiliated with any particular geography? The convention is for two letter TLDs to exactly match ISO country codes, so that may not be an option.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/dns"&gt;dns&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/domains"&gt;domains&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;&lt;/p&gt;



</summary><category term="dns"/><category term="domains"/><category term="hacker-news"/></entry><entry><title>New improved commit messages for scrape-hacker-news-by-domain</title><link href="https://simonwillison.net/2024/Sep/6/improved-commit-messages-csv-diff/#atom-tag" rel="alternate"/><published>2024-09-06T05:40:01+00:00</published><updated>2024-09-06T05:40:01+00:00</updated><id>https://simonwillison.net/2024/Sep/6/improved-commit-messages-csv-diff/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain/issues/6"&gt;New improved commit messages for scrape-hacker-news-by-domain&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
My &lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain"&gt;simonw/scrape-hacker-news-by-domain&lt;/a&gt; repo has a very specific purpose. Once an hour it scrapes the Hacker News &lt;a href="https://news.ycombinator.com/from?site=simonwillison.net"&gt;/from?site=simonwillison.net&lt;/a&gt; page (and the equivalent &lt;a href="https://news.ycombinator.com/from?site=datasette.io"&gt;for datasette.io&lt;/a&gt;) using my &lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper&lt;/a&gt; tool and stashes the parsed links, scores and comment counts in JSON files in that repo.&lt;/p&gt;
&lt;p&gt;It does this mainly so I can subscribe to GitHub's Atom feed of the commit log - visit &lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain/commits/main"&gt;simonw/scrape-hacker-news-by-domain/commits/main&lt;/a&gt; and add &lt;code&gt;.atom&lt;/code&gt; to the URL to get that.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://netnewswire.com/"&gt;NetNewsWire&lt;/a&gt; will inform me within about an hour if any of my content has made it to Hacker News, and the repo will track the score and comment count for me over time. I wrote more about how this works in &lt;a href="https://simonwillison.net/2022/Mar/14/scraping-web-pages-shot-scraper/#scrape-a-web-page"&gt;Scraping web pages from the command line with shot-scraper&lt;/a&gt; back in March 2022.&lt;/p&gt;
&lt;p&gt;Prior to the latest improvement, the commit messages themselves were pretty uninformative. The message had the date, and to actually see which Hacker News post it was referring to, I had to click through to the commit and look at the diff.&lt;/p&gt;
&lt;p&gt;I built my &lt;a href="https://github.com/simonw/csv-diff"&gt;csv-diff&lt;/a&gt; tool a while back to help address this problem: it can produce a slightly more human-readable version of a diff between two CSV or JSON files, ideally suited for including in a commit message attached to a &lt;a href="https://simonwillison.net/tags/git-scraping/"&gt;git scraping&lt;/a&gt; repo like this one.&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain/commit/35aa3c6c03507d89dd2eb7afa54839b2575b0e33"&gt;got that working&lt;/a&gt;, but there was still room for improvement. I recently learned that any Hacker News thread has an undocumented URL at &lt;code&gt;/latest?id=x&lt;/code&gt; which displays the most recently added comments at the top.&lt;/p&gt;
&lt;p&gt;I wanted that in my commit messages, so I could quickly click a link to see the most recent comments on a thread.&lt;/p&gt;
&lt;p&gt;So... I added one more feature to &lt;code&gt;csv-diff&lt;/code&gt;: a new &lt;a href="https://github.com/simonw/csv-diff/issues/38"&gt;--extra option&lt;/a&gt; lets you specify a Python format string to be used to add extra fields to the displayed difference.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain/blob/main/.github/workflows/scrape.yml"&gt;GitHub Actions workflow&lt;/a&gt; now runs this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;csv-diff simonwillison-net.json simonwillison-net-new.json \
  --key id --format json \
  --extra latest 'https://news.ycombinator.com/latest?id={id}' \
  &amp;gt;&amp;gt; /tmp/commit.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generates the diff between the two versions, using the &lt;code&gt;id&lt;/code&gt; property in the JSON to tie records together. It adds a &lt;code&gt;latest&lt;/code&gt; field linking to that URL.&lt;/p&gt;
&lt;p&gt;The commits now &lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain/commit/bda23fc358d978392d38933083ba1c49f50c107a"&gt;look like this&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Fri Sep 6 05:22:32 UTC 2024. 1 row changed. id: 41459472 points: &amp;quot;25&amp;quot; =&amp;gt; &amp;quot;27&amp;quot; numComments: &amp;quot;7&amp;quot; =&amp;gt; &amp;quot;8&amp;quot; extras: latest: https://news.ycombinator.com/latest?id=41459472" src="https://static.simonwillison.net/static/2024/hacker-news-commit.jpg" /&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/git-scraping"&gt;git-scraping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shot-scraper"&gt;shot-scraper&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="json"/><category term="projects"/><category term="github-actions"/><category term="git-scraping"/><category term="shot-scraper"/></entry><entry><title>Quoting dang</title><link href="https://simonwillison.net/2024/Aug/12/dang/#atom-tag" rel="alternate"/><published>2024-08-12T22:04:18+00:00</published><updated>2024-08-12T22:04:18+00:00</updated><id>https://simonwillison.net/2024/Aug/12/dang/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://news.ycombinator.com/item?id=41228935#41229558"&gt;&lt;p&gt;We had to exclude [dead] and eventually even just [flagged] posts from the public API because many third-party clients and sites were displaying them as if they were regular posts. […]&lt;/p&gt;
&lt;p&gt;IMO this issue is existential for HN. We've spent years and so much energy trying to find a balance between openness and human decency, a task which oscillates between barely-possible and simply-doomed, so the idea that anybody anywhere sees anything labeled "Hacker News" that pours all the toxic waste back into the ecosystem is physically painful to me.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://news.ycombinator.com/item?id=41228935#41229558"&gt;dang&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/moderation"&gt;moderation&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="moderation"/></entry><entry><title>Hacker News homepage with links to comments ordered by most recent first</title><link href="https://simonwillison.net/2024/Jul/15/hacker-news-homepage-with-links/#atom-tag" rel="alternate"/><published>2024-07-15T17:48:07+00:00</published><updated>2024-07-15T17:48:07+00:00</updated><id>https://simonwillison.net/2024/Jul/15/hacker-news-homepage-with-links/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://observablehq.com/@simonw/hacker-news-homepage"&gt;Hacker News homepage with links to comments ordered by most recent first&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Conversations on Hacker News are displayed as a tree, which can make it difficult to spot new comments added since the last time you viewed the thread.&lt;/p&gt;
&lt;p&gt;There's a workaround for this using the &lt;a href="https://hn.algolia.com/"&gt;Hacker News Algolia Search&lt;/a&gt; interface: search for &lt;code&gt;story:STORYID&lt;/code&gt;, select "comments" and the result will be a list of comments sorted by most recent first.&lt;/p&gt;
&lt;p&gt;I got fed up of doing this manually so I built a quick tool in an Observable Notebook that documents the hack, provides a UI for pasting in a Hacker News URL to get back that search interface link and also shows the most recent items on the homepage with links to their most recently added comments.&lt;/p&gt;
&lt;p&gt;See also my &lt;a href="https://til.simonwillison.net/hacker-news/recent-comments"&gt;How to read Hacker News threads with most recent comments first&lt;/a&gt; TIL from last year.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=40969925"&gt;Show HN&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="projects"/><category term="observable"/></entry><entry><title>Exploring Hacker News by mapping and analyzing 40 million posts and comments for fun</title><link href="https://simonwillison.net/2024/May/10/exploring-hacker-news-by-mapping-and-analyzing-40-million-posts/#atom-tag" rel="alternate"/><published>2024-05-10T16:42:55+00:00</published><updated>2024-05-10T16:42:55+00:00</updated><id>https://simonwillison.net/2024/May/10/exploring-hacker-news-by-mapping-and-analyzing-40-million-posts/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.wilsonl.in/hackerverse/"&gt;Exploring Hacker News by mapping and analyzing 40 million posts and comments for fun&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A real tour de force of data engineering. Wilson Lin fetched 40 million posts and comments from the Hacker News API (using Node.js with a custom multi-process worker pool) and then ran them all through the &lt;code&gt;BGE-M3&lt;/code&gt; embedding model using RunPod, which let him fire up ~150 GPU instances to get the whole run done in a few hours, using a custom RocksDB and Rust queue he built to save on Amazon SQS costs.&lt;/p&gt;
&lt;p&gt;Then he crawled 4 million linked pages, embedded &lt;em&gt;that&lt;/em&gt; content using the faster and cheaper &lt;code&gt;jina-embeddings-v2-small-en&lt;/code&gt; model, ran UMAP dimensionality reduction to render a 2D map and did a whole lot of follow-on work to identify topic areas and make the map look good.&lt;/p&gt;
&lt;p&gt;That's not even half the project - Wilson built several interactive features on top of the resulting data, and experimented with custom rendering techniques on top of canvas to get everything to render quickly.&lt;/p&gt;
&lt;p&gt;There's so much in here, and both the code and data (multiple GBs of arrow files) are available if you want to dig in and try some of this out for yourself.&lt;/p&gt;
&lt;p&gt;In the Hacker News comments Wilson shares that the total cost of the project was a couple of hundred dollars.&lt;/p&gt;
&lt;p&gt;One tiny detail I particularly enjoyed - unrelated to the embeddings - was this trick for testing which edge location is closest to a user using JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const edge = await Promise.race(
  EDGES.map(async (edge) =&amp;gt; {
    // Run a few times to avoid potential cold start biases.
    for (let i = 0; i &amp;lt; 3; i++) {
      await fetch(`https://${edge}.edge-hndr.wilsonl.in/healthz`);
    }
    return edge;
  }),
);
&lt;/code&gt;&lt;/pre&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=40307519"&gt;Show HN&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/embeddings"&gt;embeddings&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jina"&gt;jina&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="embeddings"/><category term="jina"/></entry><entry><title>Everything Google's Python team were responsible for</title><link href="https://simonwillison.net/2024/Apr/27/everything-googles-python-team-were-responsible-for/#atom-tag" rel="alternate"/><published>2024-04-27T18:52:32+00:00</published><updated>2024-04-27T18:52:32+00:00</updated><id>https://simonwillison.net/2024/Apr/27/everything-googles-python-team-were-responsible-for/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://news.ycombinator.com/item?id=40176338"&gt;Everything Google&amp;#x27;s Python team were responsible for&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
In a questionable strategic move, Google laid off the majority of their internal Python team &lt;a href="https://social.coop/@Yhg1s/112332127058328855"&gt;a few days ago&lt;/a&gt;. Someone on Hacker News asked what the team had been responsible for, and team member zem relied with this fascinating comment providing detailed insight into how the team worked and indirectly how Python is used within Google.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="google"/><category term="hacker-news"/><category term="python"/></entry><entry><title>Quoting wkirby on Hacker News</title><link href="https://simonwillison.net/2024/Apr/16/wkirby-on-hacker-news/#atom-tag" rel="alternate"/><published>2024-04-16T19:49:16+00:00</published><updated>2024-04-16T19:49:16+00:00</updated><id>https://simonwillison.net/2024/Apr/16/wkirby-on-hacker-news/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://news.ycombinator.com/item?id=40052729#40054080"&gt;&lt;p&gt;Permissions have three moving parts, who wants to do it, what do they want to do, and on what object. Any good permission system has to be able to efficiently answer any permutation of those variables. Given this person and this object, what can they do? Given this object and this action, who can do it? Given this person and this action, which objects can they act upon?&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://news.ycombinator.com/item?id=40052729#40054080"&gt;wkirby on Hacker News&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/permissions"&gt;permissions&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="permissions"/></entry><entry><title>Quoting dang</title><link href="https://simonwillison.net/2024/Feb/19/dang/#atom-tag" rel="alternate"/><published>2024-02-19T15:57:50+00:00</published><updated>2024-02-19T15:57:50+00:00</updated><id>https://simonwillison.net/2024/Feb/19/dang/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://news.ycombinator.com/context?id=39426902"&gt;&lt;p&gt;Spam, and its cousins like content marketing, could kill HN if it became orders of magnitude greater—but from my perspective, it isn't the hardest problem on HN. [...]&lt;/p&gt;
&lt;p&gt;By far the harder problem, from my perspective, is low-quality comments, and I don't mean by bad actors—the community is pretty good about flagging and reporting those; I mean lame and/or mean comments by otherwise good users who don't intend to and don't realize they're doing that.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://news.ycombinator.com/context?id=39426902"&gt;dang&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/moderation"&gt;moderation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/social-software"&gt;social-software&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/spam"&gt;spam&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="moderation"/><category term="social-software"/><category term="spam"/></entry><entry><title>Analytics: Hacker News v.s. a tweet from Elon Musk</title><link href="https://simonwillison.net/2023/Feb/17/analytics/#atom-tag" rel="alternate"/><published>2023-02-17T22:11:44+00:00</published><updated>2023-02-17T22:11:44+00:00</updated><id>https://simonwillison.net/2023/Feb/17/analytics/#atom-tag</id><summary type="html">
    &lt;p&gt;My post &lt;a href="https://simonwillison.net/2023/Feb/15/bing/"&gt;Bing: “I will not harm you unless you harm me first”&lt;/a&gt; really took off.&lt;/p&gt;
&lt;p&gt;It sat &lt;a href="https://news.ycombinator.com/item?id=34804874"&gt;at the top of Hacker News&lt;/a&gt; for a full day, and is currently &lt;a href="https://hn.algolia.com/"&gt;the 18th most popular post&lt;/a&gt; of all time on that site.&lt;/p&gt;
&lt;p&gt;And then this happened:&lt;/p&gt;

&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;Might need a bit more polish …&lt;a href="https://t.co/rGYCxoBVeA"&gt;https://t.co/rGYCxoBVeA&lt;/a&gt;&lt;/p&gt;- Elon Musk (@elonmusk) &lt;a href="https://twitter.com/elonmusk/status/1625936009841213440?ref_src=twsrc%5Etfw"&gt;February 15, 2023&lt;/a&gt;&lt;/blockquote&gt;

&lt;p&gt;Given &lt;a href="https://www.theverge.com/2023/2/14/23600358/elon-musk-tweets-algorithm-changes-twitter"&gt;recent changes&lt;/a&gt; made to the Twitter algorithm, a &lt;em&gt;lot&lt;/em&gt; of people saw that. Twitter currently reports 30.4M views of that tweet.&lt;/p&gt;
&lt;p&gt;A bunch of people asked me how much of that converted into page views. So let's dive in!&lt;/p&gt;
&lt;h4&gt;Headline figures&lt;/h4&gt;
&lt;p&gt;Here's my Plausible dashboard for that post over the past few days:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/plausible-bing.jpg" alt="simonwillison.net on Plausible, filtered for /2023/Feb/15/bing/ - there's a huge spike in traffic starting on the 16th of Feb. 959k unique visitors, 1.1M page views, 90% bounce rate, 42m43s time on page. Top sources of traffic are Twitter at 721k, Direct / None at 132k, Hacker News at 49.5k, Facebook at 13.4k, Reddit at 8.3x, Google at 7.8k, tldrnewsletter at 6k and LinkedIn at 5.4k" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Overall numbers: 959k unique visitors, 1.1M page views.&lt;/p&gt;
&lt;p&gt;Top sources of traffic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Twitter: 721k&lt;/li&gt;
&lt;li&gt;Direct / None: 132k (this includes traffic from Mastodon)&lt;/li&gt;
&lt;li&gt;Hacker News: 49.5k&lt;/li&gt;
&lt;li&gt;Facebook: 13.4k&lt;/li&gt;
&lt;li&gt;Reddit: 8.3k&lt;/li&gt;
&lt;li&gt;Google: 7.8k&lt;/li&gt;
&lt;li&gt;tldrnewsletter: 6k&lt;/li&gt;
&lt;li&gt;LinkedIn: 5.4k&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If we assume the vast majority of the Twitter traffic was from Elon (which seems reasonable) that's 30.4M / 721k = roughly a 2.37% click through rate.&lt;/p&gt;
&lt;p&gt;Notable that sticking at the top of Hacker News for a day really does drive an enormous amount of traffic - 18% of the traffic you get from the second most followed account on Twitter (looks like &lt;a href="https://twitter.com/barackobama"&gt;Barack Obama&lt;/a&gt; is still number one).&lt;/p&gt;
&lt;h4&gt;More detailed analytics via Plausible and Cloudflare&lt;/h4&gt;
&lt;p&gt;I mainly use &lt;a href="https://plausible.io/"&gt;Plausible&lt;/a&gt; for my site's analytics. I really like them: they're privacy-focused, open source (though I use their hosted version) and show me exactly the subset of data I want to see. Most importantly, they don't set cookies.&lt;/p&gt;
&lt;p&gt;My site also runs behind &lt;a href="https://www.cloudflare.com/"&gt;Cloudflare&lt;/a&gt;, which also provides analytics. I don't pay for the upgraded analytics, but it turns out you can still get some pretty detailed numbers out of them - especially if you're willing to dig around in the browser DevTools.&lt;/p&gt;
&lt;p&gt;Plausible offers an "export" button, so I used that... and got a zip file with a bunch of CSVs in it. &lt;a href="https://github.com/simonw/i-will-not-harm-you-unless-you-harm-me-first/tree/main/plausible-csvs"&gt;Here they are&lt;/a&gt; in a GitHub repo.&lt;/p&gt;
&lt;p&gt;Cloudflare - at least for the free tier - doesn't have a detailed export. But... under the hood the Cloudflare web application &lt;a href="https://developers.cloudflare.com/analytics/graphql-api/"&gt;uses their GraphQL API&lt;/a&gt; to retrieve stats for display, and with a bit of digging you can get numbers out that way.&lt;/p&gt;
&lt;p&gt;I extracted &lt;a href="https://github.com/simonw/i-will-not-harm-you-unless-you-harm-me-first/blob/main/cloudflare.json"&gt;this 3.2MB JSON file&lt;/a&gt; using the Cloudflare API.&lt;/p&gt;
&lt;h4&gt;Loading it into Datasette&lt;/h4&gt;
&lt;p&gt;I wrote &lt;a href="https://github.com/simonw/i-will-not-harm-you-unless-you-harm-me-first/blob/main/build-dbs.sh"&gt;this script&lt;/a&gt; to load the data I had extracted into SQLite database files, and then deployed them to Vercel using &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can explore the result here: &lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/"&gt;https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/plausible/visitors?_sort=rowid&amp;amp;date__gte=2023-02-15#g.mark=bar&amp;amp;g.x_column=date&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=pageviews&amp;amp;g.y_type=quantitative"&gt;Here's page views according to Plausible&lt;/a&gt; over the time period in question:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/datasette-plausible-pageviews.jpg" alt="Chart in Datasette showing page views per hour according to Plausible - a big jump up to around 185,000 at 11am on the 15th" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It looks to me like the timezone for that data is Pacific Time.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/cloudflare/timeslots#g.mark=bar&amp;amp;g.x_column=timeslot&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=pageViews&amp;amp;g.y_type=quantitative"&gt;This page&lt;/a&gt; shows page views count according to Cloudflare, by hour.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/datasette-cloudflare-pageview.jpg" alt="Datasette interafce showing a chart plotted using the datasette-vega plugin - the chart shows pageviews against time spiking up to just over 200,000 at 7pm UTC on 15th Feb, the time of the Elon tweet" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This data is in UTC, where 7pm UTC corresponds to 11am Pacific.&lt;/p&gt;
&lt;p&gt;These numbers should differ, because Plausible uses JavaScript to track analytics while Cloudflare is server-side, plus Plausible is filtered to just hits to the specific page while Cloudflare is showing all hits to any page on my site.&lt;/p&gt;
&lt;p&gt;There are plenty more ways to slice and dice the data in Datasette:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/plausible/visitors?_sort=rowid&amp;amp;date__gte=2023-02-15#g.mark=bar&amp;amp;g.x_column=date&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=visitors&amp;amp;g.y_type=quantitative"&gt;Unique visitors over time according to Plausible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/cloudflare/timeslots#g.mark=bar&amp;amp;g.x_column=timeslot&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=uniques&amp;amp;g.y_type=quantitative"&gt;Uniques over time according to Cloudflare&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/plausible/sources#g.mark=bar&amp;amp;g.x_column=name&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=visitors&amp;amp;g.y_type=quantitative"&gt;Full data for those traffic sources from Plausible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/plausible/devices"&gt;Plausible device breakdown&lt;/a&gt; - 778,678 mobile, 101,216 desktop, 47,781 laptop (not sure how it distinguishes between desktop and laptop though), 16,967 tablet.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://i-will-not-harm-you-unless-you-harm-me-first.vercel.app/cloudflare?sql=select+timeslot%2C+requests%2C+cachedRequests%2C+100.0+*+cachedRequests+%2F+requests+as+pctCached+from+timeslots+order+by+timeslot+limit+101#g.mark=line&amp;amp;g.x_column=timeslot&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=pctCached&amp;amp;g.y_type=quantitative"&gt;Percentage of cached requests over time according to Cloudflare&lt;/a&gt; using a custom SQL query - this was around 40% before the Elon tweet, then jumped up to over 90% and stayed there, thankfully!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I've long been a fan of full-page HTTP caching as protection against surprise traffic events - it's a pattern I've implemented in the past using Varnish and Fastly, and I've been using it on my blog via Cloudflare for several years.&lt;/p&gt;
&lt;p&gt;It definitely paid off this time!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/analytics"&gt;analytics&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/bing"&gt;bing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/twitter"&gt;twitter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="analytics"/><category term="bing"/><category term="hacker-news"/><category term="twitter"/><category term="datasette"/><category term="cloudflare"/></entry><entry><title>Scraping web pages from the command line with shot-scraper</title><link href="https://simonwillison.net/2022/Mar/14/scraping-web-pages-shot-scraper/#atom-tag" rel="alternate"/><published>2022-03-14T01:29:56+00:00</published><updated>2022-03-14T01:29:56+00:00</updated><id>https://simonwillison.net/2022/Mar/14/scraping-web-pages-shot-scraper/#atom-tag</id><summary type="html">
    &lt;p&gt;I've added a powerful new capability to my &lt;strong&gt;&lt;a href="https://github.com/simonw/shot-scraper"&gt;shot-scraper&lt;/a&gt;&lt;/strong&gt; command line browser automation tool: you can now use it to load a web page in a headless browser, execute JavaScript to extract information and return that information back to the terminal as JSON.&lt;/p&gt;
&lt;p&gt;Among other things, this means you can construct Unix pipelines that incorporate a full headless web browser as part of their processing.&lt;/p&gt;
&lt;p&gt;It's also a really neat web scraping tool.&lt;/p&gt;
&lt;h4&gt;shot-scraper&lt;/h4&gt;
&lt;p&gt;I &lt;a href="https://simonwillison.net/2022/Mar/10/shot-scraper/"&gt;introduced shot-scraper&lt;/a&gt; last Thursday. It's a Python utility that wraps &lt;a href="https://playwright.dev/"&gt;Playwright&lt;/a&gt;, providing both a command line interface and a YAML-driven configuration flow for automating the process of taking screenshots of web pages.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% pip install shot-scraper
% shot-scraper https://simonwillison.net/ --height 800
Screenshot of 'https://simonwillison.net/' written to 'simonwillison-net.png'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/simonwillison-net.png" alt="Screenshot of my blog homepage" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Since Thursday &lt;code&gt;shot-scraper&lt;/code&gt; has had &lt;a href="https://github.com/simonw/shot-scraper/releases"&gt;a flurry of releases&lt;/a&gt;, adding features like &lt;a href="https://github.com/simonw/shot-scraper/blob/0.9/README.md#saving-a-web-page-to-pdf"&gt;PDF exports&lt;/a&gt;, the ability to dump the Chromium &lt;a href="https://github.com/simonw/shot-scraper/blob/0.9/README.md#dumping-out-an-accessibility-tree"&gt;accessibilty tree&lt;/a&gt; and the ability to take screenshots of &lt;a href="https://github.com/simonw/shot-scraper/blob/0.9/README.md#websites-that-need-authentication"&gt;authenticated web pages&lt;/a&gt;. But the most exciting new feature landed today.&lt;/p&gt;
&lt;h4&gt;Executing JavaScript and returning the result&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/shot-scraper/releases/tag/0.9"&gt;Release 0.9&lt;/a&gt; takes the tool in a new direction. The following command will execute JavaScript on the page and return the resulting value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% shot-scraper javascript simonwillison.net document.title
"Simon Willison\u2019s Weblog"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or you can return a JSON object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% shot-scraper javascript https://datasette.io/ "({
  title: document.title,
  tagline: document.querySelector('.tagline').innerText
})"
{
  "title": "Datasette: An open source multi-tool for exploring and publishing data",
  "tagline": "An open source multi-tool for exploring and publishing data"
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or if you want to use functions like &lt;code&gt;setTimeout()&lt;/code&gt; - for example, if you want to insert a delay to allow an animation to finish before running the rest of your code - you can return a promise:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% shot-scraper javascript datasette.io "
new Promise(done =&amp;gt; setInterval(
  () =&amp;gt; {
    done({
      title: document.title,
      tagline: document.querySelector('.tagline').innerText
    });
  }, 1000
));"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Errors that occur in the JavaScript turn into an exit code of 1 returned by the tool - which means you can also use this to execute simple tests in a CI flow. This example will fail a GitHub Actions workflow if the extracted page title is not the expected value:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;- &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Test page title&lt;/span&gt;
  &lt;span class="pl-ent"&gt;run&lt;/span&gt;: &lt;span class="pl-s"&gt;|-&lt;/span&gt;
&lt;span class="pl-s"&gt;    shot-scraper javascript datasette.io "&lt;/span&gt;
&lt;span class="pl-s"&gt;      if (document.title != 'Datasette') {&lt;/span&gt;
&lt;span class="pl-s"&gt;        throw 'Wrong title detected';&lt;/span&gt;
&lt;span class="pl-s"&gt;      }"&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id="scrape-a-web-page"&gt;Using this to scrape a web page&lt;/h4&gt;
&lt;p&gt;The most exciting use case for this new feature is web scraping. I'll illustrate that with an example.&lt;/p&gt;
&lt;p&gt;Posts from my blog occasionally show up on &lt;a href="https://news.ycombinator.com/"&gt;Hacker News&lt;/a&gt; - sometimes I spot them, sometimes I don't.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://news.ycombinator.com/from?site=simonwillison.net"&gt;https://news.ycombinator.com/from?site=simonwillison.net&lt;/a&gt; is a Hacker News page showing content from the specified domain. It's really useful, but it sadly isn't included in the official &lt;a href="https://github.com/HackerNews/API"&gt;Hacker News API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/news-ycombinator-com-from.png" alt="Screenshot of the Hacker News listing for my domain" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;So... let's write a scraper for it.&lt;/p&gt;
&lt;p&gt;I started out running the Firefox developer console against that page, trying to figure out the right JavaScript to extract the data I was interested in. I came up with this:&lt;/p&gt;

&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-v"&gt;Array&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;from&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelectorAll&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.athing'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;title&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.titleline a'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerText&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;points&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;parseInt&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;nextSibling&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.score'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerText&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.titleline a'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;href&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;dt&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;nextSibling&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.age'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;title&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;submitter&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;nextSibling&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.hnuser'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerText&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;commentsUrl&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;nextSibling&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.age a'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;href&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;id&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;commentsUrl&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;split&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'?id='&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;1&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Only posts with comments have a comments link&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;commentsLink&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Array&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;from&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
    &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;nextSibling&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelectorAll&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'a'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
  &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;filter&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;el&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerText&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;includes&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'comment'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;numComments&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;commentsLink&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;numComments&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;parseInt&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;commentsLink&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerText&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;split&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;id&lt;span class="pl-kos"&gt;,&lt;/span&gt; title&lt;span class="pl-kos"&gt;,&lt;/span&gt; url&lt;span class="pl-kos"&gt;,&lt;/span&gt; dt&lt;span class="pl-kos"&gt;,&lt;/span&gt; points&lt;span class="pl-kos"&gt;,&lt;/span&gt; submitter&lt;span class="pl-kos"&gt;,&lt;/span&gt; commentsUrl&lt;span class="pl-kos"&gt;,&lt;/span&gt; numComments&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The great thing about modern JavaScript is that everything you could need to write a scraper is already there in the default environment.&lt;/p&gt;
&lt;p&gt;I'm using &lt;code&gt;document.querySelectorAll('.itemlist .athing')&lt;/code&gt; to loop through each element that matches that selector.&lt;/p&gt;
&lt;p&gt;I wrap that with &lt;code&gt;Array.from(...)&lt;/code&gt; so I can use the &lt;code&gt;.map()&lt;/code&gt; method. Then for each element I can extract out the details that I need.&lt;/p&gt;
&lt;p&gt;The resulting array contains 30 items that look like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;[
  {
    &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;30658310&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"title"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Track changes to CLI tools by recording their help output&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"url"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;https://simonwillison.net/2022/Feb/2/help-scraping/&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"dt"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2022-03-13T05:36:13&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"submitter"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;appwiz&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"commentsUrl"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;https://news.ycombinator.com/item?id=30658310&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"numComments"&lt;/span&gt;: &lt;span class="pl-c1"&gt;19&lt;/span&gt;
  }
]&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;Running it with shot-scraper&lt;/h4&gt;
&lt;p&gt;Now that I have a recipe for a scraper, I can run it in the terminal like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;shot-scraper javascript &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://news.ycombinator.com/from?site=simonwillison.net&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;Array.from(document.querySelectorAll('.athing'), el =&amp;gt; {&lt;/span&gt;
&lt;span class="pl-s"&gt;  const title = el.querySelector('.titleline a').innerText;&lt;/span&gt;
&lt;span class="pl-s"&gt;  const points = parseInt(el.nextSibling.querySelector('.score').innerText);&lt;/span&gt;
&lt;span class="pl-s"&gt;  const url = el.querySelector('.titleline a').href;&lt;/span&gt;
&lt;span class="pl-s"&gt;  const dt = el.nextSibling.querySelector('.age').title;&lt;/span&gt;
&lt;span class="pl-s"&gt;  const submitter = el.nextSibling.querySelector('.hnuser').innerText;&lt;/span&gt;
&lt;span class="pl-s"&gt;  const commentsUrl = el.nextSibling.querySelector('.age a').href;&lt;/span&gt;
&lt;span class="pl-s"&gt;  const id = commentsUrl.split('?id=')[1];&lt;/span&gt;
&lt;span class="pl-s"&gt;  // Only posts with comments have a comments link&lt;/span&gt;
&lt;span class="pl-s"&gt;  const commentsLink = Array.from(&lt;/span&gt;
&lt;span class="pl-s"&gt;    el.nextSibling.querySelectorAll('a')&lt;/span&gt;
&lt;span class="pl-s"&gt;  ).filter(el =&amp;gt; el &amp;amp;&amp;amp; el.innerText.includes('comment'))[0];&lt;/span&gt;
&lt;span class="pl-s"&gt;  let numComments = 0;&lt;/span&gt;
&lt;span class="pl-s"&gt;  if (commentsLink) {&lt;/span&gt;
&lt;span class="pl-s"&gt;    numComments = parseInt(commentsLink.innerText.split()[0]);&lt;/span&gt;
&lt;span class="pl-s"&gt;  }&lt;/span&gt;
&lt;span class="pl-s"&gt;  return {id, title, url, dt, points, submitter, commentsUrl, numComments};&lt;/span&gt;
&lt;span class="pl-s"&gt;})&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; simonwillison-net.json&lt;/pre&gt;&lt;/div&gt;  
&lt;p&gt;&lt;code&gt;simonwillison-net.json&lt;/code&gt; is now a JSON file containing the scraped data.&lt;/p&gt;
&lt;h4&gt;Running the scraper in GitHub Actions&lt;/h4&gt;
&lt;p&gt;I want to keep track of changes to this data structure over time. My preferred technique for that is something I call &lt;a href="https://simonwillison.net/2020/Oct/9/git-scraping/"&gt;Git scraping&lt;/a&gt; - the core idea is to keep the data in a Git repository and commit an update any time it updates. This provides a cheap and robust history of changes over time.&lt;/p&gt;
&lt;p&gt;Running the scraper in GitHub Actions means I don't need to administrate my own server to keep this running.&lt;/p&gt;
&lt;p&gt;So I built exactly that, in the &lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain"&gt;simonw/scrape-hacker-news-by-domain&lt;/a&gt; repository.&lt;/p&gt;
&lt;p&gt;The GitHub Actions workflow is in &lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain/blob/485841482a39869759e39f4d8dee21b9adc963d7/.github/workflows/scrape.yml"&gt;.github/workflows/scrape.yml&lt;/a&gt;. It runs the above command once an hour, then pushes a commit back to the repository should the file have any changes since last time it ran.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain/commits/main/simonwillison-net.json"&gt;commit history of simonwillison-net.json&lt;/a&gt; will show me any time a new link from my site appears on Hacker News, or a comment is added.&lt;/p&gt;
&lt;p&gt;(Fun GitHub trick: add &lt;code&gt;.atom&lt;/code&gt; to the end of that URL to get &lt;a href="https://github.com/simonw/scrape-hacker-news-by-domain/commits/main/simonwillison-net.json.atom"&gt;an Atom feed of those commits&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;The whole scraper, from idea to finished implementation, took less than fifteen minutes to build and deploy.&lt;/p&gt;
&lt;p&gt;I can see myself using this technique &lt;em&gt;a lot&lt;/em&gt; in the future.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scraping"&gt;scraping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/git-scraping"&gt;git-scraping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shot-scraper"&gt;shot-scraper&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cli"/><category term="github"/><category term="hacker-news"/><category term="scraping"/><category term="github-actions"/><category term="git-scraping"/><category term="shot-scraper"/></entry><entry><title>Launch HN Instructions</title><link href="https://simonwillison.net/2021/Jul/19/launch-hn-instructions/#atom-tag" rel="alternate"/><published>2021-07-19T01:05:37+00:00</published><updated>2021-07-19T01:05:37+00:00</updated><id>https://simonwillison.net/2021/Jul/19/launch-hn-instructions/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://news.ycombinator.com/yli.html"&gt;Launch HN Instructions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The instructions for YC companies that are posting their launch announcement on Hacker News are really interesting to read. “As founders, you’re used to talking to users, customers, and investors. HN readers are not any of those—what they are is peers, and using any of those styles with peers feels clueless and entitled. [...]  To interest HN, write in a factual, personal, and modest way about what problem you solve, why it matters, how you solve it, and how you got there.”

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=27877280"&gt;dang&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/marketing"&gt;marketing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/y-combinator"&gt;y-combinator&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="marketing"/><category term="y-combinator"/></entry><entry><title>hacker-news-to-sqlite 0.4</title><link href="https://simonwillison.net/2021/Mar/13/hacker-news-to-sqlite/#atom-tag" rel="alternate"/><published>2021-03-13T19:15:06+00:00</published><updated>2021-03-13T19:15:06+00:00</updated><id>https://simonwillison.net/2021/Mar/13/hacker-news-to-sqlite/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/dogsheep/hacker-news-to-sqlite/releases/tag/0.4"&gt;hacker-news-to-sqlite 0.4&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="hacker-news"/><category term="sqlite"/></entry><entry><title>A List of Hacker News's Undocumented Features and Behaviors</title><link href="https://simonwillison.net/2020/Jun/6/hacker-news-undocumented/#atom-tag" rel="alternate"/><published>2020-06-06T17:36:40+00:00</published><updated>2020-06-06T17:36:40+00:00</updated><id>https://simonwillison.net/2020/Jun/6/hacker-news-undocumented/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/minimaxir/hacker-news-undocumented/blob/master/README.md"&gt;A List of Hacker News&amp;#x27;s Undocumented Features and Behaviors&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
If you’re interested in community software design this is a neat insight into the many undocumented features of Hacker News, collated by Max Woolf.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=23439437"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/community"&gt;community&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/max-woolf"&gt;max-woolf&lt;/a&gt;&lt;/p&gt;



</summary><category term="community"/><category term="hacker-news"/><category term="max-woolf"/></entry><entry><title>SQL is a better API language than GraphQL – Convince me otherwise</title><link href="https://simonwillison.net/2020/Apr/16/sql-is-a-better-api-language-than-graphql/#atom-tag" rel="alternate"/><published>2020-04-16T22:44:55+00:00</published><updated>2020-04-16T22:44:55+00:00</updated><id>https://simonwillison.net/2020/Apr/16/sql-is-a-better-api-language-than-graphql/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://news.ycombinator.com/item?id=22892946"&gt;SQL is a better API language than GraphQL – Convince me otherwise&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A &lt;a href="https://twitter.com/simonw/status/1250803209871847426"&gt;flippant tweet&lt;/a&gt; I posted this morning blew up today and ended up on the Hacker News homepage.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1250803209871847426"&gt;My thread on Twitter&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webapis"&gt;webapis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/graphql"&gt;graphql&lt;/a&gt;&lt;/p&gt;



</summary><category term="hacker-news"/><category term="sql"/><category term="webapis"/><category term="graphql"/></entry><entry><title>hacker-news-to-sqlite 0.3.1</title><link href="https://simonwillison.net/2020/Mar/21/hacker-news-to-sqlite-2/#atom-tag" rel="alternate"/><published>2020-03-21T22:41:16+00:00</published><updated>2020-03-21T22:41:16+00:00</updated><id>https://simonwillison.net/2020/Mar/21/hacker-news-to-sqlite-2/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/dogsheep/hacker-news-to-sqlite/releases/tag/0.3.1"&gt;hacker-news-to-sqlite 0.3.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hacker-news"&gt;hacker-news&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="hacker-news"/><category term="sqlite"/></entry></feed>