Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
47
When I copy a browser tab URL, I often want to also keep the title. Sometimes I want to use the link as rich text (e.g., when pasting the link into OneNote or Jira). Sometimes I prefer a Markdown link. There are browser extensions to achieve this task, but I don't want to introduce potential security issues. Instead, I've written a bookmarklet based on this example extension. To use it, drag the following link onto your browser bookmarks bar: Copy Tab When you click the bookmark(let), the current page including its title will be copied into your clipboard. You don't even have to choose the output format: the link is copied both as rich text and plain text (Markdown). This works because it's possible to write multiple values into the clipboard with different content types. Here's the source code: function escapeHTML(str) { return String(str) .replace(/&/g, "&amp;") .replace(/"/g, "&quot;") .replace(/'/g, "&#39;") .replace(/</g, "&lt;") .replace(/>/g,...
a month ago

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from Darek Kay

Open Graph images: Format compatibility across platforms

While redesigning my photography website, I've looked into the Open Graph (OG) images, which are displayed when sharing a link on social media or messaging apps. Here's an example from WhatsApp: For each photo that I publish, I create a WebP thumbnail for the gallery. I wanted to use those as OG images, but the WebP support was lacking, so I've been creating an additional JPG variant just for Open Graph. I was interested in seeing how things have changed in the last 2.5 years. I've tested the following platforms: WhatsApp, Telegram, Signal, Discord, Slack, Teams, Facebook, LinkedIn, Xing, Bluesky, Threads and Phanpy (Mastodon). Here are the results: All providers support JPEG and PNG. All providers except Teams and Xing support WebP. No provider except Facebook supports AVIF. WhatsApp displays the AVIF image, but the colors are broken. "X, formerly Twitter" didn't display OG images for my test pages at all. I don't care about that platform, so I didn't bother to further investigate. Those results confirmed that I could now use WebP Open Graph images without creating an additional JPG file.

2 months ago 55 votes
A guide to bookmarklets

I'm a frequent user of bookmarklets. As I'm sharing some of them on my blog, I wrote this post to explain what bookmarklets are and how to use them. In short, a bookmarklet is a browser bookmark containing JavaScript code. Clicking the bookmark executes the script in the context of the current web page, allowing users to perform tasks such as modifying the appearance of a webpage or extracting information. Bookmarklets are a simpler, more lightweight alternative to browser extensions, Chrome snippets, and userscripts. How to add a bookmarklet? Here's an example to display a browser dialog with the title of the current web page: Display page title You can click the link to see what it does. To run this script on other websites, we have to save it as a bookmarklet. My preferred way is to drag the link onto the bookmarks toolbar: A link on a web page is dragged and dropped onto a browser bookmark bar. A bookmark creation dialog appears. The prompt is confirmed and closed. The created bookmarklet is clicked. The current web page title is displayed in a browser dialog. Another way is to right-click the link to open its context menu: In Firefox, you can then select "Bookmark Link…". Other browsers make it a little more difficult: select "Copy Link (Address)", manually create a new bookmark, and then paste the copied URL as the link target. Once created, you can click the bookmark(let) on any web page to display its title. Scroll further down to see more useful use cases. How to write a bookmarklet? Let's start with the code for the previous bookmarklet example: window.alert(document.title) To turn that script into a bookmarklet, we have to put javascript: in front of it: javascript:window.alert(document.title) To keep our code self-contained, we should wrap it with an IIFE (immediately invoked function expression): javascript:(() => { window.alert(document.title) })() Finally, you might have to URL-encode your bookmarklet if you get issues with special characters: javascript:%28%28%29%20%3D%3E%20%7B%0A%20%20window.alert%28document.title%29%0A%7D%29%28%29 Useful bookmarklets Here are some bookmarklets I've created: Debugger — Starts the browser DevTools debugger after 3 seconds, useful for debugging dynamic content changes. Log Focus Changes — Logs DOM elements when the focus changes. Design Mode — Makes the web page content-editable (toggle).

3 months ago 57 votes
Prevent data loss on page refresh

It can be frustrating to fill out a web form, only to accidentally refresh the page (or click "back") and lose all the hard work. In this blog post, I present a method to retain form data when the page is reloaded, which improves the user experience. Browser behavior Most browsers provide an autofill feature. In the example form below, enter anything into the input field. Then, try out the following: Click the "Example link" and use the "back" functionality of your browser. Reload the page. Query Example link Depending on your browser, the input value might be restored: Browser Reload Back Firefox 130 Yes Yes Chrome 129 No Yes Safari 18 No Yes How does it work? I was surprised to learn that this autofill behavior is controlled via the autocomplete, that is mostly used for value autocompletion from past web forms. However, if we disable the autocompletion, the autofill feature will be disabled as well: <input autocomplete="false" /> To learn more about the behavior, read the full spec on persisted history entry state. Preserving application state Even with autofill, no browser will restore dynamic changes previously triggered by the user. In the following example, the user has to always press the "Search" button to view the results: This is an interactive example. Please enable JavaScript to use it. Query Search ... const inputElementNative = document.querySelector("#example-search-input"); const outputElementNative = document.querySelector("#example-search-output"); const performSearch_exampleSearch = (outputElement) => { outputElement.innerText = ""; const introText = document.createTextNode("Open the result for "); outputElement.appendChild(introText); const link = document.createElement("a"); link.href = "https://example.com"; link.innerText = inputElementNative.value || "no text"; outputElement.appendChild(link); }; document.querySelector("#example-search-form").addEventListener("submit", (event) => { event.preventDefault(); performSearch_exampleSearch(outputElementNative); }, ); If the web page changes its content after user interaction, it might be a good idea to restore the UI state after the page has been refreshed. For example, it's useful to restore previous search results for an on-site search. Note that Chrome will fire a change event on inputs, but this is considered a bug as the respective spec has been updated. Storing form values As the form value might be lost on reload, we need to store it temporarily. Some common places to store data include local storage, session storage, cookies, query parameters or hash. They all come with drawbacks for our use case, though. Instead, I suggest using the browser history state, which has several advantages: We get data separation between multiple browser tabs with no additional effort. The data is automatically cleaned up when the browser tab is closed. We don't pollute the URL and prevent page reloads. Let's store the search input value as query: document.querySelector("form").addEventListener("submit", (event) => { event.preventDefault(); const inputElement = document.querySelector("input"); history.replaceState({ query: inputElement.value }, ""); performSearch(); }); This example uses the submit event to store the data, which fits our "search" use case. In a regular form, using the input change event might be a better trigger to store form values. Using replaceState over pushState will ensure that no unnecessary history entry is created. Uncaught TypeError: Failed to execute 'replaceState' on 'History': 2 arguments required, but only 1 present. Restoring form values My first approach to restore form values was to listen to the pageshow event. Once it's fired, we can access the page load type from window.performance: window.addEventListener("pageshow", () => { const type = window.performance.getEntriesByType("navigation")[0].type; const query = history.state?.query; if (query && (type === "back_forward" || type === "reload")) { document.querySelector("#my-input").value = query; performSearch(); } }); I will keep the solution here in case someone needs it, but usually it is unnecessary to check the page load type. Because the history state is only set after the search form has been submitted, we can check the state directly: const query = history.state?.query; if (query) { document.querySelector("#my-input").value = query; performSearch(); } Demo Here's an example combining both techniques to store and restore the input value: This is an interactive example. Please enable JavaScript to use it. Query Search ... const inputElementCustom = document.querySelector("#example-preserve-input"); const outputElementCustom = document.querySelector("#example-preserve-output"); const performSearch_examplePreserve = (outputElement) => { outputElement.innerText = ""; const introText = document.createTextNode("Open the result for "); outputElement.appendChild(introText); const link = document.createElement("a"); link.href = "https://example.com"; link.innerText = inputElementCustom.value || "no text"; outputElement.appendChild(link); }; document.querySelector("#example-preserve-form").addEventListener("submit", (event) => { event.preventDefault(); performSearch_examplePreserve(outputElementCustom); history.replaceState({ query: inputElementCustom.value }, ""); }); const historyQuery = history.state?.query; if (historyQuery) { document.querySelector("#example-preserve-input").value = historyQuery; performSearch_examplePreserve(outputElementCustom); } Conclusion Preserving form data on page refresh is a small but impactful way to improve user satisfaction. The default browser autofill feature handles only basic use cases, so ideally we should maintain the form state ourselves. In this blog post, I've explained how to use the browser history state to temporarily store and retrieve form values.

4 months ago 57 votes
Web push notifications: issues and limitations

In this post, I will summarize some problems and constraints that I've encountered with the Notifications and Push web APIs. Notification settings on macOS Someone who's definitely not me wasted half an hour wondering why triggered notifications would not appear. On macOS, make sure to enable system notifications for your browsers. Open "System Settings" → "Notifications". For each browser, select "Allow notifications" and set the appearance to "Alerts": Onchange listener not called Web APIs offer a way to subscribe to change events. This is especially useful in React: navigator.permissions .query({ name: "push", userVisibleOnly: true }) .then((status) => { status.onchange = function () { // synchronize permission status with local state setNotificationPermission(this.state); }; }); Whenever the notification permission changes (either through our application logic or via browser controls), we can synchronize the UI in real-time according to the current permission value (prompt, denied or granted). However, due to a Firefox bug, the event listener callback is never called. This means that we can't react to permission changes via browser controls in Firefox. That's especially unfortunate when combined with push messages, where we want to subscribe the user once they grant the notification permission. One workaround is to check at page load if the notification permission is granted with no valid subscription and resubscribe the user. Notification image not supported Browser notifications support an optional image property. This property is marked as "experimental", so it's not surprising that some browsers (Firefox, Safari) don't support it. There is an open feature request to add support in Firefox, but it has been open since 2019. VAPID contact information required When sending a push message, we have to provide VAPID parameters (e.g. the public and private key). According to the specification, the sub property (contact email or link) is optional: If the application server wishes to provide, the JWT MAY include a "sub" (Subject) claim. Despite this specification, the Mozilla push message server will return an error if the subject is missing: 401 Unauthorized for (...) and subscription https://updates.push.services.mozilla.com/wpush/v2/… You might not encounter this issue when using the popular web-push npm package, as its API encourages you to provide the subject as the first parameter: webpush.setVapidDetails("hello@example.com", publicKey, privateKey); However, in the webpush-java library, you need to set the subject explicitly: builder.subject("hello@example.com"); There is an open issue with more information about this problem. Microsoft Edge pitfalls Microsoft introduced adaptive notification requests in the Edge browser. It is a crowdsourced score system, which may auto-accept or auto-reject notification requests. The behavior can be changed in the Edge notification settings. Additionally, on a business or school device, those settings might be fixed, displaying the following tooltip: This setting is managed by your organization.

5 months ago 57 votes

More in programming

Making inventory spreadsheets for my LEGO sets

One of my recent home organisation projects has been sorting out my LEGO collection. I have a bunch of sets which are mixed together in one messy box, and I’m trying to separate bricks back into distinct sets. My collection is nowhere near large enough to be worth sorting by individual parts, and I hope that breaking down by set will make it all easier to manage and store. I’ve been creating spreadsheets to track the parts in each set, and count them out as I find them. I briefly hinted at this in my post about looking at images in spreadsheets, where I included a screenshot of one of my inventory spreadsheets: These spreadsheets have been invaluable – I can see exactly what pieces I need, and what pieces I’m missing. Without them, I wouldn’t even attempt this. I’m about to pause this cleanup and work on some other things, but first I wanted to write some notes on how I’m creating these spreadsheets – I’ll probably want them again in the future. Getting a list of parts in a set There are various ways to get a list of parts in a LEGO set: Newer LEGO sets include a list of parts at the back of the printed instructions You can get a list from LEGO-owned website like LEGO.com or BrickLink There are community-maintained databases on sites like Rebrickable I decided to use the community maintained lists from Rebrickable – they seem very accurate in my experience, and you can download daily snapshots of their entire catalog database. The latter is very powerful, because now I can load the database into my tools of choice, and slice and dice the data in fun and interesting ways. Downloading their entire database is less than 15MB – which is to say, two-thirds the size of just opening the LEGO.com homepage. Bargain! Putting Rebrickable data in a SQLite database My tool of choice is SQLite. I slept on this for years, but I’ve come to realise just how powerful and useful it can be. A big part of what made me realise the power of SQLite is seeing Simon Willison’s work with datasette, and some of the cool things he’s built on top of SQLite. Simon also publishes a command-line tool sqlite-utils for manipulating SQLite databases, and that’s what I’ve been using to create my spreadsheets. Here’s my process: Create a Python virtual environment, and install sqlite-utils: python3 -m venv .venv source .venv/bin/activate pip install sqlite-utils At time of writing, the latest version of sqlite-utils is 3.38. Download the Rebrickable database tables I care about, uncompress them, and load them into a SQLite database: curl -O 'https://cdn.rebrickable.com/media/downloads/colors.csv.gz' curl -O 'https://cdn.rebrickable.com/media/downloads/parts.csv.gz' curl -O 'https://cdn.rebrickable.com/media/downloads/inventories.csv.gz' curl -O 'https://cdn.rebrickable.com/media/downloads/inventory_parts.csv.gz' gunzip colors.csv.gz gunzip parts.csv.gz gunzip inventories.csv.gz gunzip inventory_parts.csv.gz sqlite-utils insert lego_parts.db colors colors.csv --csv sqlite-utils insert lego_parts.db parts parts.csv --csv sqlite-utils insert lego_parts.db inventories inventories.csv --csv sqlite-utils insert lego_parts.db inventory_parts inventory_parts.csv --csv The inventory_parts table describes how many of each part there are in a set. “Set S contains 10 of part P in colour C.” The parts and colors table contains detailed information about each part and color. The inventories table matches the official LEGO set numbers to the inventory IDs in Rebrickable’s database. “The set sold by LEGO as 6616-1 has ID 4159 in the inventory table.” Run a SQLite query that gets information from the different tables to tell me about all the parts in a particular set: SELECT ip.img_url, ip.quantity, ip.is_spare, c.name as color, p.name, ip.part_num FROM inventory_parts ip JOIN inventories i ON ip.inventory_id = i.id JOIN parts p ON ip.part_num = p.part_num JOIN colors c ON ip.color_id = c.id WHERE i.set_num = '6616-1'; Or use sqlite-utils to export the query results as a spreadsheet: sqlite-utils lego_parts.db " SELECT ip.img_url, ip.quantity, ip.is_spare, c.name as color, p.name, ip.part_num FROM inventory_parts ip JOIN inventories i ON ip.inventory_id = i.id JOIN parts p ON ip.part_num = p.part_num JOIN colors c ON ip.color_id = c.id WHERE i.set_num = '6616-1';" --csv > 6616-1.csv Here are the first few lines of that CSV: img_url,quantity,is_spare,color,name,part_num https://cdn.rebrickable.com/media/parts/photos/9999/23064-9999-e6da02af-9e23-44cd-a475-16f30db9c527.jpg,1,False,[No Color/Any Color],Sticker Sheet for Set 6616-1,23064 https://cdn.rebrickable.com/media/parts/elements/4523412.jpg,2,False,White,Flag 2 x 2 Square [Thin Clips] with Chequered Print,2335pr0019 https://cdn.rebrickable.com/media/parts/photos/15/2335px13-15-33ae3ea3-9921-45fc-b7f0-0cd40203f749.jpg,2,False,White,Flag 2 x 2 Square [Thin Clips] with Octan Logo Print,2335pr0024 https://cdn.rebrickable.com/media/parts/elements/4141999.jpg,4,False,Green,Tile Special 1 x 2 Grille with Bottom Groove,2412b https://cdn.rebrickable.com/media/parts/elements/4125254.jpg,4,False,Orange,Tile Special 1 x 2 Grille with Bottom Groove,2412b Import that spreadsheet into Google Sheets, then add a couple of columns. I add a column image where every cell has the formula =IMAGE(…) that references the image URL. This gives me an inline image, so I know what that brick looks like. I add a new column quantity I have where every cell starts at 0, which is where I’ll count bricks as I find them. I add a new column remaining to find which counts the difference between quantity and quantity I have. Then I can highlight or filter for rows where this is non-zero, so I can see the bricks I still need to find. If you’re interested, here’s an example spreadsheet that has a clean inventory. It took me a while to refine the SQL query, but now I have it, I can create a new spreadsheet in less than a minute. One of the things I’ve realised over the last year or so is how powerful “get the data into SQLite” can be – it opens the door to all sorts of interesting queries and questions, with a relatively small amount of code required. I’m sure I could write a custom script just for this task, but it wouldn’t be as concise or flexible. [If the formatting of this post looks odd in your feed reader, visit the original article]

20 hours ago 3 votes
Giving Junior Engineers Control Of A Six Trillion Dollar System Is Nuts

For some purpose, the DOGE people are burrowing their way into all US Federal Systems. Their complete control over the Treasury Department is entirely insane. Unless you intend to destroy everything, making arbitrary changes to complex computer systems will result in destruction, even if that was not your intention. No

5 hours ago 3 votes
Stanislav Petrov

A lieutenant colonel in the Soviet Air Defense Forces prevented the end of human civilization on September 26th, 1983. His name was Stanislav Petrov. Protocol dictated that the Soviet Union would retaliate against any nuclear strikes sent by the United States. This was a policy of mutually assured destruction, a doctrine that compels a horrifying logical conclusion. The second and third stage effects of this type of exchange would be even more catastrophic. Allies for each side would likely be pulled into the conflict. The resulting nuclear winter was projected to lead to 2 billion deaths due to starvation. This is to say nothing about those who would have been unfortunate enough to have survived. Petrov’s job was to monitor Oko, the computerized warning systems built to centralize Soviet satellite communications. Around midnight, he received a report that one of the satellites had detected the infrared signature of a single launch of a United States ICBM. While Petrov was deciding what to do about this report, the system detected four more incoming missile launches. He had minutes to make a choice about what to do. It is impossible to imagine the amount of pressure placed on him at this moment. Source: Stanislav Petrov, Soviet officer credited with averting nuclear war, dies at 77 by Schwartzreport. Petrov lived in a world of deterministic systems. The technologies that powered these warning systems have outputs that are guaranteed, provided the proper inputs are provided. However, deterministic does not mean infallible. The only reason you are alive and reading this is because Petrov understood that the systems he observed were capable of error. He was suspicious of what he was seeing reported, and chose not to escalate a retaliatory strike. There were two factors guiding his decision: A surprise attack would most likely have used hundreds of missiles, and not just five. The allegedly foolproof Oko system was new and prone to errors. An error in a deterministic system can still lead to expected outputs being generated. For the Oko system, infrared reflections of the sun shining off of the tops of clouds created a false positive that was interpreted as detection of a nuclear launch event. Source: US-K History by Kosmonavtika. The concept of erroneous truth is a deep thing to internalize, as computerized systems are presented as omniscient, indefective, and absolute. Petrov’s rewards for this action were reprimands, reassignment, and denial of promotion. This was likely for embarrassing his superiors by the politically inconvenient shedding of light on issues with the Oko system. A coerced early retirement caused a nervous breakdown, likely him having to grapple with the weight of his decision. It was only in the 1990s—after the fall of the Soviet Union—that his actions were discovered internationally and celebrated. Stanislav Petrov was given the recognition that he deserved, including being honored by the United Nations, awarded the Dresden Peace Prize, featured in a documentary, and being able to visit a Minuteman Missile silo in the United States. On January 31st, 2025, OpenAI struck a deal with the United States government to use its AI product for nuclear weapon security. It is unclear how this technology will be used, where, and to what extent. It is also unclear how OpenAI’s systems function, as they are black box technologies. What is known is that LLM-generated responses—the product OpenAI sells—are non-deterministic. Non-deterministic systems don’t have guaranteed outputs from their inputs. In addition, LLM-based technology hallucinates—it invents content with no self-knowledge that it is a falsehood. Non-deterministic systems that are computerized also have the perception as being authoritative, the same as their deterministic peers. It is not a question of how the output is generated, it is one of the output being perceived to come from a machine. These are terrifying things to know. Consider not only the systems this technology is being applied to, but also the thoughtless speed of their integration. Then consider how we’ve historically been conditioned and rewarded to interpret the output of these systems, and then how we perceive and treat skeptics. We don’t live in a purely deterministic world of technology anymore. Stanislav Petrov died on September 18th, 2017, before this change occurred. I would be incredibly curious to know his thoughts about our current reality, as well as the increasing abdication of human monitoring of automated systems in favor of notably biased, supposed “AI solutions.” In acknowledging Petrov’s skepticism in a time of mania and political instability, we acknowledge a quote from former U.S. Secretary of Defense William J. Perry’s memoir about the incident: [Oko’s false positives] illustrates the immense danger of placing our fate in the hands of automated systems that are susceptible to failure and human beings who are fallible.

yesterday 7 votes
01 · A spreadsheet for exploring scenarios

In our *Ambsheets* project, we are exploring a small extension to the familiar spreadsheet: **what if a single spreadsheet cell could hold multiple values at once**?

yesterday 2 votes
Recently

I am not going to repeat the news. But man, things are really, really bad and getting worse in America. It’s all so unendingly stupid and evil. The tech industry is being horrible, too. Wishing strength to the people who are much more exposed to the chaos than I am. Reading A Confederacy of Dunces was such a perfect novel. It was pure escapism, over-the-top comedy, and such an unusual artifact, that was sadly only appreciated posthumously. Very earnestly I believe that despite greater access to power and resources, the box labeled “socially acceptable ways to be a man” is much smaller than the box labeled “socially acceptable ways to be a woman.” This article on the distinction between patriarchy and men was an interesting read. With the whole… politics out there, it’s easy to go off the rails with any discussion about men and women and whether either have it easy or hard. The same author wrote this good article about declining male enrollment in college. I think both are worth a read. Whenever I read this kind of article, I’m reminded of how limited and mostly fortunate my own experience is. There’s a big difference, I think, in how vigorously you have to perform your gender in some red state where everyone owns a pickup truck, versus a major city where the roles are a little more fluid. Plus, I’ve been extremely fortunate to have a lot of friends and genuine open conversations about feelings with other men. I wish that was the norm! On Having a Maximum Wealth was right up my alley. I’m reading another one of the new-French-economist books right now, and am still fascinated by the prospect of wealth taxes. My friend David has started a local newsletter for Richmond, Virginia, and written a good piece about public surveillance. Construction Physics is consistently great, and their investigation of why skyscrapers are all glass boxes is no exception. Watching David Lynch was so great. We watched his film Lost Highway a few days after he passed, and it was even better than I had remembered it. Norm Macdonald’s extremely long jokes on late-night talk shows have been getting me through the days. Listening This song by the The Hard Quartet – a supergroup of Emmett Kelly, Stephen Malkmus (Pavement), Matt Sweeney and Jim White. It’s such a loving, tender bit of nonsense, very golden-age Pavement. They also have this nice chill song: I came across this SML album via Hearing Things, which has been highlighting a lot of good music. Small Medium Large by SML It’s a pretty good time for these independent high-quality art websites. Colossal has done the same for the art world and highlights good new art: I really want to make it out to see the Nick Cave (not the musician) art show while it’s in New York.

yesterday 1 votes