Posts in Web-development (20 found)

Pidgin Markup For Writing, or How Much Can HTML Sustain?

Read on the website: HTML is flexible and was shaped by generations of web practitioners. It has enough tricks up its sleeve to actually be nice to author. Here are some.

0 views
Maurycy Yesterday

How to write your own website:

I recently wrote an essay on why you should set up a personal website rather then using social media. Doing so lets you own your space on the internet, customize it and free your readers from constant advertising and algorithmic feeds designed to keep you stuck doomscrolling all day. However, despite how much time we spend using it, creating something for the intenet is seen as arcane wizardy by most people. This is a fairly accessable guide to getting started. You’ll need a text editor (any will do) and a browser (you already have one). All pages are written in HTML, which is a simple text-based format. To start with, this is a perfectly valid HTML document: To try this, just create a text file with a ".html" extension, and open it in your favorite browser. Do this now : experimenting is the best way to learn how everything works. This is what it should look like: Plain text is boring, so let’s add some formatting: The angle bracket things are tags: "<b>" is an opening tag, and "</b>" is the matching closing tag. The word surrounded by brackets ("b") is the tag name, which tells the browser what to do: In this case, b olding the enclosed text. The other formatting tags are <em> em phasis , <u> u nderline , <sub> sub scipt , <sup> sup erscript , <small> small text , <mark> highlight and <del> del eted . You don’t have to memorize this list, but go and try a few out. There’s also <br/> ( br eak), which adds a line break. It’s special because there’s no closing tag: It always immediately closed and can’t contain any text. I like to add a slash after the tag name to indicate this A big wall of text can get quite ugly, so it’s good to break it up with <p> ( p aragraph) tags. Each paragraph will be visually separated from other content on the page: Check out my new site: I have many epic things here. Together, the maching tags and their contents form an an element . Elements can contain other elements, but it’s important that they are closed in the correct order: This is wrong: … but this is fine: Browsers will attempt to render invalid HTML, but the results may not be what you intended: It’s best to make it easy for them. On that topic, it’s good practice to put all your content inside a <body> element which is itself inside a <html> element: Check out my new site: I have many epic things here. This isn’t mandatory, but helps browsers render your page correctly: In the case of an old browser, you don’t want metadata (we’ll add some later) getting confused for page content. Ok, back to text-wall-avoidance: the <ul> and <ol> ( u nordered/ o rdered l ist) tags create, well, lists. Each item should be wraped in <li> tags ( l ist i tem) About this site (unordered): It has epic things ... and is handwritten HTML It uses these tags: (ordered) <html> <body> <p> <ul> and <ol> <li> You can add angle brackets to a page with &gt; (>), &lt; (<) and &amp; (&). These entities will render as the corresponding charater, but won’t form tags. Headings use <h1> ( h eading 1 ) through <h5> ( h eading 5 ), with larger numbers using smaller font sizes: This site has epic things and I wrote it myself. To do: Figure out how to add links. About that. Links are just <a> ( a nchor) tags, but they have something new: an attribute after the tag name but before the bracket. The "href= " attribute sets where the link points to. A lot of other tags can also have attributes: For example, ordered lists with "reverse=true" count backwards. The URL in "href=" can be relative: If linking up multiple pages on the same site, instead of this: … just write this: Images work similarly to links, except that they are self-closing elements like <br/>: Check out this picture of a nebula I took! (If you don’t have a URL for your image, skip to the hosting section to set one up) That’s all the essentials, but there’s a lot of other useful tags. For example <details> creates a dropdown that works with ctrl-f: This is a dropdown with just HTML. It works well with browser features (ctrl-f, fragment identifiers, screen readers, etc) by default. (better usability than 99% of commercial sites!) …but I can’t cover everything without writing a whole book. (The Mozzila docs are a fantastic reference) At this point, you should have something like this: I made this site to write about things I do. More updates soon™ . Here's my picture of the Dumbbell Nebula: Let’s start by giving the page a machine-readable title: Like with <body>, the <head> tag isn’t required, but it is good to include it: Otherwise, any metadata that the browser doesn’t understand might be mistaken for content. The page still looks kinda bad: Text extending the edges of the page isn’t exactly easy to read. It’s not too bad when crammed into my blog, but longer paragraphs will look terrible on large monitors. To fix this, we need to add some style and layout information using the <style> tag: Unlike other tags, the contents of <style> isn’t HTML, but CSS: a whole other langauge embedded within the file. CSS is compoosed of blocks, each begining with a selector to control what gets effected. Here, this is just the name of a tag: "head" The selector is followed by a series of declarations wraped in curly braces. My example only has one: "max-width: 30em;" This caps the width of the element at 30 times the font size: I made this site to write about things I do. More updates soon™ . Here's my picture of the Dumbbell Nebula: The page is looking rather asymetrical, so let’s center the column. For fixed-width elements, this can be done using the "margin" property: I made this site to write about things I do. More updates soon™ . Here's my picture of the Dumbbell Nebula: (For varable width elements, use flexbox for centering and other fancy layouts. A single line of text can be centered with "text-align=center") Personally, I like dark themed sites, so lets change some of the colors: I made this site to write about things I do. More updates soon™ . Here's my picture of the Dumbbell Nebula: The "color" style will carry over to every element inside of the styled tag, so there’s no need to individually change the text-color of every element. However, the links do need to be changed because they override the color by default. That’s it. Everything you need to replicate my blog, minus a few small bits like the sans-serif font, nagivation box, etc. Of course, your website can and should be different: It’s yours . I highly recomend you read some documenation and play around with CSS. There’s also way more to it then I can possbly cover here. Every website you see was created with it, and it even supports animations and basic interactivity . … also, check out your browser’s devtools (ctrl-shift-i): It will have a nice GUI for editing which shows you the result in real time and shows you what’s going on under the hood. If you ever run out of tags, you can just make up your own and style them as needed. As long as the name includes a hypen, it’s guaranteed not to be included in any future version of HTML. The specification even lists <math-α> and <emotion-😍> as allowed custom elements names. I’ve used this heavily on this page: All the example websites aren’t screenshots, they are <fake-frame> elements styled up to look like a browser window. Custom tags are also very handy for styling text: At this point you should have a reasonably nice page ready to put up on the internet. The easiest way to do this is to use a static file hosting service like Github Pages or Cloudflare Pages . Both of these have generous free tiers that should last a very long time. If you don’t like big companies, there are plenty of similar, smaller services. These can be more limited: The popular Neocities charges $5/mo to use a custom domain. Another option is to rent a server ($3-$5/mo) or, if you have good internet, run one yourself. This is by far the most fiddly option: I would not recommend it unless you like playing with computers. All off these (except a server) will give you a subdomain by default. For example, Github Pages will give you your-username .github.io However, I do recommend setting up a custom domain: This will let you switch providers seamlessly should anything happen. All of these will work in a similar way: Upload a file with some name, and it will given a URL with that same name. The one exception is that files called "index.html" will be viewable at the root of the folder they are in. You should put an index.html in the root of your site to serve as the homepage, but apart from that, the organization is up to you. It has epic things ... and is handwritten HTML <html> <body> <ul> and <ol> Ken Shirriff's blog Ken Shirriff's blog Ken Shirriff's blog Ken Shirriff's blog

0 views
Thomasorus Yesterday

2025

Despite everything happening in the world, 2025 was one of the happiest years of my recent life. I feel that a lot of my efforts paid off, making my life richer, more interesting, and allowing me to learn more about myself. My partner's disability was recognized by the state. She receives a temporary pension so we don't have to stress about money as much. She had surgery, recovered swiftly, and can walk again. Therefor, my role as a caregiver was reduced immensely, a relief as we enter our 20th year together! As for my own health, it's getting better. Pain in my back, shoulders, hands and arms diminished thanks to exercise. I was able to rule-out a few concerns I had (among them tachycardia and nerve issues) thanks to exams, and others are ongoing. Despite a lot of troubles at work, going from management issues, toxic clients, or people quitting, I'm still feeling OK. I am actually detached and it feels great. I still care enough to work at the best of my abilities but once I close my laptop, work disappears. But I am getting very bored by frontend web development tho, and asked if I could evolve into project management. I had a positive response, but we'll have to discuss it further. I won't do it if the company doesn't accept a best effort obligation. Taking up the role without having the means to do it is a trap I refuse to fall into. And finally, my non-monogamous life stabilized. I now understand what I am looking for, I am able to explain it properly. It led to long term lover-friends relationships, with people who are all emotionally mature, caring, and are overall amazing and interesting human beings. A thing that didn't stabilize is my brain. I had several moments that felt not right . Among them: My theory, as for why it happens now and more, is that I am slowly getting rid of a performative social attitude I built years ago. For the last 20, maybe even 30 years I spent a lot of energy into pleasing others, doing things right, being a reliable friend, colleague and spouse. To the detriment of my own well being and sometimes, health. I am unsure why I work like this. Could be anxiety, past experiences, educations, role models. Who knows? Almost everyone around me, including psychiatrists, think I have ADHD. It lead to a huge mental breakdown . I'll be seeing a neuropsychiatrist in a few weeks and try to find out what's going on. This year marked a radical shift: I am actually listening to my needs, wants and obsessions. Allowing myself to be... well, myself, has made me extremely happy and fulfilled this year. I regained a lot of freedom to do new things, made a lot of encounters that led to new activities, experiences and discoveries. It can go from little things like wearing earrings, buying new clothes or doing new piercings ; to more fundamental shifts like trying out stressful social gatherings, asking others to do things for my own benefit, or abandoning moral positions that actually led me to infantilize or disrespect the individuality of others. Seeing my loved ones respond positively to these changes lifted a huge amount of stress from my shoulders. For all these reasons, I can say 2025 was among the best years of my life. I haven't felt this alive, this myself for years, and I hope it will continue for a long time. The urge for something, anything, to happen, right now . I crave adventure and novelty, but also stress when outside a familiar environment. Feeling overwhelmed and overstimulated by the number of things in my environment, mostly objects but also sounds, lights, etc. An inability to find, use and sometimes think about things if they are not directly visible. For example, messy storage becomes just a mass of indistinct things I cannot manage and I give up. The feeling of never doing enough while never defining what is enough . Happened especially in social and profesional settings. An increasing difficulty to focus on tasks that aren't interesting to me, to the point of forgetting about them. Stopped my 15 years old podcast + website dedicated to fighting games. Stopped my 7 years old freelance shop. David Lynch died and it affected me more than I expected (here's an interview of him I enjoyed). Exhibited and sol my art for the first time and had a blast full blog post . Tried to code with AI and became stupid and lazy . Migrated this website from a static site generator to my own CMS built in GO . I started documenting the steps for some drawings I did. You can see the creative process behind Drool and Mandala in their respective pages. Failed Inktober/Drawtober due to sickness. I focused on a fantasy setting I had in mind for a few years, both writing and drawing elements of this world. Keep being a little more me every day. Learn to let go for real. Continue to balance equally taking care of myself and others. Nurture my relationships with others. Participate in an art fare with a broader range of art than last year. Write more on my website about various things. Finish my CMS. Go on vacation alone again, and with my partner. Refurnish and redecorate more rooms in my apartment. Keep exploring fashion to find my own style.

0 views
neilzone 3 days ago

My initial thoughts about Immich, a self-hosted photo gallery

I am looking to move away from Nextcloud (another blogpost at another time), and one of the things that I wanted to replace was a tool for automatically uploading photos from my phone (Graphene OS) and Sandra’s phone (an iPhone). At the moment, they are uploaded automatically to Nextcloud, although I’m not sure how well that works on Sandra’s iPhone. I also take a backup, using rsync to copy photos from the phones to an external drive , when I remember, which frankly is not often enough. But I did one of these backups ahead of installing Immich, which was useful. Lastly, I have a load of photos from the last 20 years, including our wedding photos, saved onto two different drives, and I wanted to add those to the hosted backup. I had heard a lot about Immich , with happy users, so I decided to give it a go. I did not really look around for alternatives. I already had a machine suitable to run Immich, so that was straightforward. Immich is, to my slight annoyance, only available via docker. I’m not a fan of docker, as I don’t understand it well enough. I can just about use it, but if there is a problem, I don’t have many skills beyond “bring it down then bring it up again”. I certainly haven’t mastered docker’s networking. But anyway, it is available via docker. So, naturally, I used and . I followed the official instructions , substituting “podman” for “docker”, and it worked fine. Since I am using an external drive to store the photo library, I configured that in the file. And there we go. Immich was running. For TLS termination, I use nginx, which I already had running on that machine anyway. The official suggested nginx configuration worked for http, but for whatever reason, I could not get it to work with to sort out a TLS certificate. I didn’t spend too much time debugging this, as I was not convinced I’d be able to get it to work. Instead, I started from the default nginx config, then used certbot, then put bits back in from the Immich-suggested config. I tested it with , to make sure I hadn’t somehow messed it up, and it was working, so fingers crossed. I set up an admin user, and then two normal users, for Sandra and me. I didn’t do any other specific configuration. Immich does not offer multi-factor authentication, which I find surprising, but ho hum, since it is an internal-only service, it is not a big deal for me. Perhaps I need to make Authelia or similar a future project. I set up the Android app, via F-Droid , and the iPhone app from the App Store. I decided to upload my Camera library without getting into albums, so there was no particular configuration other than choosing the Camera directory. Uploading ~3000 photos from my phone took a while, but it worked fine. For Sandra’s iPhone, I set it up, and set up the directory to sync, but I didn’t wait for it to upload. Instead - and for the other photos which I wanted to upload to Immich - I used the backup copy I had on an external drive. I used the command line tool immich-go for this. After creating both a user API key and an admin API key, I just left it to do its thing. I had to restart it a few times (and switch user API key for Sandra’s photos, to upload them to her account), but after a few runs, I had uploaded everything. I managed to upload all of my photos before I went to bed last night and, this morning, the server had created all the thumbnails, and done the facial recognition (not sure how I feel about this). I expect Sandra’s to be ready tomorrow. I have quite a few photos without a location, because they were taken many years ago on an old-fashioned digital camera. Batch replacement of location is reasonably straightforward, and I have not gone for the exact specific location, but at least the right town / city. I have fixed the date on some photos, but I need to go through more of them. 1 January is a very popular date. As far as I know, there is no way to fix incorrect orientation right now , but I think that that is coming pretty soon. So far, I am impressed. Despite docker, it was pretty straightforward to get running, and I am willing to chalk my problems with nginx down to me. I will probably use the web interface rather than the app, and that seems pretty good. Sandra will probably use the iOS app and, again, that seems fine. As long as it uploads photos automatically - which is something to test / keep an eye on - I suspect that we will be happy with it.

2 views

The frustration of a perfect setup

No matter how I look at the list of apps I currently use , whether first-party or third-party, I can’t find anything to change, not a program to replace, not a service to swap for another. I think I am happy with my setup. It feels strange to admit, but somehow, I can’t quite believe it; I must be missing something, something surely can be tweaked. What happens after peak setup? This frustration comes from the fact that looking at new apps, digging into settings, trying new online services, working on how each of these things operates with the others, is one of my favourite hobbies. I mean, a quick glance at the archive of this site will tell you that, not only do I love writing about apps and digital tools, but I love playing with their configurations; I’m like a kid with Lego bricks, building things, taking them apart, and building them again, with a huge smile, in a slightly different and improved way. Now that my application setup appears to be “final”, it feels as though all my toys and Lego bricks are neatly stored away in their respective drawers, sorted by colour, by type, and by size. It’s perfect, and seeing my beautiful collection all nice and tidy like that is a very satisfying sensation, except I’m looking at it seated on the empty floor of my childhood bedroom, alone and bored. What is there to do when nothing needs to be improved? I recently wrote about my HTML and CSS “explorations” with this blog. Satisfied with the results, I think this job is done. The same goes for how Eleventy works on my machine: everything has been optimised , refined, future-proofed (especially Node.js ): nothing to see here! Even the hosting is something I’m very happy with. My only gripe with xmit is that there is no possibility for me to pay for it. The other apps on my Mac — the ones that don’t live in the Terminal like Eleventy, Node.js & npm, and xmit — are also perfect at what they do, and I can’t think of anything better to explore, let alone to use. If this is not your first visit, you already know how I feel about BBEdit . Well, I feel just about the same about NetNewsWire , which is as close to perfection an app can get as far as I’m concerned. It feels part of the OS (even more so than current system apps if I’m being honest), it is stable, it is simple to use, and it runs smoothly on my soon-to-be six-year-old MacBook Air. Being happy with Safari is by far the strongest proof that my setup is final. Using StopTheScript to block JavaScript on most media sites, along with the performance and privacy benefits of using a DNS resolver like Quad9 , has proven to be an efficient way to keep Safari light and responsive, even if my web experience is getting a little more interrupted than I would like, due to all the crap websites throw at first-time visitors these days. Yesterday, I had a look at apps like Yoink , Karabiner Elements , Hazel , and also got a taste of Mullvad Browser , and News Explorer . Some of these apps were tried purely out of curiosity, to see if they would fit right in my “workflow”, others were basically reassurance that my current system and choices were the best I could have made. * 1 Among all the parties involved in this setup, the obvious candidate for a replacement is my Intel-powered MacBook Air. Yet, this old computer is currently in great shape: the recent factory-settings reset I had to do surely helped. But its best feature is not being able to run MacOS Tahoe: stuck to MacOS Sequoia, it’s protecting me from Liquid Glass on the Mac and the “icons in menus everywhere” experience. My personal laptop is a breath of fresh air after spending hours on my work computer running Tahoe. * 2 So, what will be able to make that itch go away? When nothing is broken, don’t fix it, as they say. But surely, there must be something that I’m missing, surely there is a program, somewhere, that would delight me, that would put a smile on my face. I want a new box of Lego bricks, I want to empty my drawers on the floor and see if I can do better. In case you’re wondering, all of these apps are excellent, but not enough to replace what I already use, or to justify adding a new item to my list. For example, Mullvad Browser, like Firefox, isn’t scriptable; News Explorer has more features than NetNewsWire, but is not as polished; Yoink looks incredibly useful, but I prefer my own ways for now, &c. ^ Its replacement will have to wait until the new generation comes out, probably in March; then I can decide on whether I want to stick to the Air family, keep mine a bit longer, or upgrade for a far nicer screen and go with the Pro. ^ In case you’re wondering, all of these apps are excellent, but not enough to replace what I already use, or to justify adding a new item to my list. For example, Mullvad Browser, like Firefox, isn’t scriptable; News Explorer has more features than NetNewsWire, but is not as polished; Yoink looks incredibly useful, but I prefer my own ways for now, &c. ^ Its replacement will have to wait until the new generation comes out, probably in March; then I can decide on whether I want to stick to the Air family, keep mine a bit longer, or upgrade for a far nicer screen and go with the Pro. ^

0 views
devansh 4 days ago

ElysiaJS Cookie Signature Validation Bypass

The recent React CVE(s) made quite a buzz in the industry. It was a pretty powerful vulnerability, which directly leads to Pre-auth RCE (one of the most impactful vuln classes). The React CVE inspired me to investigate vulnerabilities in other JS/TS frameworks. I selected Elysia as my target for several reasons: active maintenance, ~16K GitHub stars, clear documentation, and clean codebase - all factors that make for productive security research. While scrolling through the codebase, one specific codeblock looked interesting: It took me less than a minute to identify the "anti-pattern" here. Can you see what's wrong here? We'll get to it in a bit, but first, a little primer on ElysiaJS Cookie Signing. Elysia treats cookies as reactive signals, meaning they're mutable objects you can read and update directly in your route handlers without getters/setters. Cookie signing adds a cryptographic layer to prevent clients from modifying cookie values (e.g., escalating privileges in a session token). Elysia uses a signature appended to the cookie value, tied to a secret key. This ensures integrity (data wasn't altered) and authenticity (it came from your server). On a higher level, it works something like this: Rotating secrets is essential for security hygiene (e.g., after a potential breach or periodic refresh). Elysia handles this natively with multi-secret support . This code is responsible for handling cookie related logic (signing, unsigning, secrets rotation). Now, going back to the vulnerability, can you spot the vulnerability in the below screenshot? No worries if you couldn't. I will walk you through. The guard check at the end ( ) becomes completely useless because can never be . This is dead code. You see now? Basically if you are using the vulnerable version of Elysia and using secrets array (secrets rotation); Complete auth bypass is possible because error never gets thrown. This seemed like a pretty serious issue, so I dropped a DM to Elysia's creator SaltyAom . SaltyAom quickly confirmed the issue At this point, we know that this is a valid issue, but we still need to create a PoC for it to showcase what it can do, so a security advisory could be created. Given my limited experience with Tyscript. I looked into the docs of Elysia and looked into sample snippets. After getting a decent understanding of syntax Elysia uses, it was time to create the PoC app using Elysia. I had the basic idea in my mind of how my PoC app would look like, It will have a protected resource only admin can access, and by exploiting this vulnerability I should be able to reach the protected resource without authenticating as admin or without even having admin cookies. Eventually, I came up with the following PoC for demonstrating impact: Without signing up as admin, or login, issue the following cURL command: We got access to protected content; without using an signed admin cookie. Pretty slick, no? The developer likely meant to write: Instead, they wrote: The attacker only needs to: That's literally it. This vulnerability was fixed in v1.4.19 With this fix in place, the verification logic now works correctly. Affected Versions : Elysia ≤ v1.4.18 ( confirmed ), potentially earlier versions Fixed Versions : v1.4.19 Elysia and Cookie Signing Secrets Rotation Vulnerability Proof of Concept What It Does Let's Break It Disclosure Timeline cookies.ts#L413-L426 Signing : When you set a cookie (e.g., profile.value = data), Elysia hashes the serialized value + secret, appends sig to the cookie. Unsigning/Verification : On read, Elysia checks the signature against the secret. If invalid (tampered or wrong secret), it throws an error or rejects the cookie. How It Works: Provide secrets as an array: [oldestDeprecated, ..., currentActive]. Tries the latest secret first for signing new cookies. For reading, it falls back sequentially through the array until a match (or fails). Sets ( assumes the cookie is valid before checking anything! ) Loops through each secret Calls for each secret If any secret successfully verifies, sets ( wait, it's already - this does nothing ), stores the unsigned value, and breaks If no secrets verify , the loop completes naturally without ever modifying Checks if is ... but it's still from step 1 No error is thrown - the tampered cookie is accepted as valid Allows one-time signup of an admin account only Allows an existing admin to log in . Issues a signed session cookie once logged in. Protects a secret route so only logged-in admin can access it. Capture or observe one valid cookie ( even their own ) Edit the cookie value to some other users' identify in their browser or with curl; and remove the signature Send it back to the server Discovery : 9th December 2025 Vendor Contact : 9th December 2025 Vendor Response : 9th December 2025 Patch Release : 13th December 2025 CVE Assignment : Pending Vulnerable Code: src/cookies.ts#L413-L426 Elysia Documentation: elysiajs.com Elysia Cookie Documentation: elysiajs.com/patterns/cookie

0 views
Jeff Geerling 5 days ago

Local Email Debugging with Mailpit

For the past decade, I've used Mailhog for local email debugging. Besides working on web applications that deal with email, I've long used email as the primary notification system for comments on the blog. I built an Ansible role for Mailhog , and it was one of the main features of Drupal VM , a popular local development environment for Drupal I sunset 3 years ago. Unfortunately, barring any future updates from the maintainers, it seems like Mailhog has not been maintained for four years now. It still works , but something as complex as an email debugging environment needs ongoing maintenance to stay relevant.

0 views
JSLegendDev 1 weeks ago

The Phaser Game Framework in 5 Minutes

Phaser is the most popular JavaScript/TypeScript framework for making 2D games. It’s performant, and popular games like Vampire Survivors and PokéRogue were made with it. Because it’s a web-native framework, games made with Phaser are lightweight and generally load and run better on the web than the web exports produced by major game engines. For that reason, if you’re looking to make 2D web games, Phaser is a great addition to your toolbelt. In this post, I’ll explain the framework’s core concepts in around 5 minutes. — SPONSORED SEGMENT — In case you want to bring your web game to desktop platforms, today’s sponsor GemShell , allows you to build executables for Windows/Mac/Linux in what amounts to a click. It also makes Steam integration easy. For more info, visit 👉 https://l0om.itch.io/gemshell You have a tool/product you want featured in a sponsored segment? Contact me at [email protected] A Phaser project starts with defining a config to describe how the game’s canvas should be initialized. To make the game scale according to the window’s size, we can set the scale property in our config. The mode property is set to FIT, so the canvas scales while preserving its own aspect ratio. As for keeping the canvas centered on the page, the autoCenter property is used with the CENTER_BOTH value. Most games are composed of multiple scenes and switching between them is expected during the course of gameplay. Since Phaser uses the object oriented paradigm, a scene is created by defining a class that inherits from the Phaser.Scene class. To be able to reference the scene elsewhere in our code, it’s important to give it a key. For this purpose and for being able to use the methods and properties of the parent class we need to call the super constructor and pass to it the key we want to use. The two most important methods of a Phaser scene are the and methods. The first is used for, among other things, creating game objects like text and sprites and setting things like scores. It runs once, every time the scene becomes active. The latter which runs once per frame is, for example, used to handle movement logic. Once a scene is created, we still need to add it to our game. This is done in the Phaser config under a property called scenes, which expects an array. The order of scenes in this array is important. The first element will be used as the default scene of the game. To switch scenes, we can call the method of the scene manager. Before we can render sprites, we need to load them. For this purpose, a Phaser scene has access to the method where asset loading logic should be placed. To load an image, we can use the image method of the loader plugin. Then, in the method, we can render a sprite by calling the method of the Game Object Factory plugin. The first two params are for specifying the X and Y coordinates while the third param is for providing the key of the sprite to render. Because we created our sprite game object in the method, we don’t have access to it in our method, that’s why you’ll often see the pattern of assigning a game object to an instance field so it becomes accessible to other methods of the scene. Finally, movement logic code is placed in the method which runs every frame. Rendering text is similar to sprites. Rather than the using the method, we use the method. If you want to hold data or define custom methods for a sprite game object, a better approach is to define a class that inherits from the Phaser.GameObject.Sprite class. Once the class is defined, we can use it in our scene’s code. While asset loading can be done in any Phaser scene, a better approach is to create a scene dedicated to loading assets, which then switches to the main game scene once loading is complete. This can be achieved like shown below : Another important aspect of any game is the ability to play animations. Usually for 2D games, we have spritesheets containing all the needed frames to animate a character in a single image. An example of a spritesheet In Phaser, we first specify the dimensions of a frame in the loading logic of the spritesheet so that the framework knows how to slice the image into individual frames. Then, we can create an animation by defining its starting and ending frames. To provide the needed frames we call the method of Phaser’s animation manager. Finally once the animation is created, it can be played by using the method of the sprite game object. If you want the animation to loop back indefinitely, add the repeat property and set it to -1. A game needs to be interactive to be called a “game”. One way to handle input is by using event listeners provided by Phaser. For keyboard input, we can use : And for handling mouse and touch input we can use . At one point, you might need to share data between scenes. For this purpose, you can use Phaser’s registry. Here is an example of its usage. To play sounds (assuming you have already loaded the sound first) you can use the method of the sound manager. You can specify the sound’s volume in the second param of that method. If you need to be able to stop, pause or play the same sound at a later time, you can add it to the sound manager rather than playing it immediately. This comes in handy when you transition from one scene to another and you have a sound that loops indefinitely. In that case, you need to stop the sound before switching over otherwise the sound will keep playing in the next scene. By default, Phaser offers an Arcade physics system which is not meant for complex physics simulations. However, it’s well suited for most types of games. To enable it, you can add the following to your Phaser config. You can add an existing game object to the physics system the same way you add one to a scene. This will create a physics body for that game object which is accessible with the body instance field. You can view this body as a hitbox around your sprite if you turn on the debug mode in your project’s config. Example of a Phaser game with debug set to true To create bodies that aren’t affected by gravity, like platforms, you can create a static group and then create and add static bodies to that group. Here’s an example : You can also add already existing physics bodies to a group. Now, you might be wondering what groups are useful for? They shine in collision handling logic. Let’s assume you have multiple enemies attacking the player. To determine when a collision occurs between any enemy and the player, you can set up the following collision handler : There are many concepts I did not have time to cover. If you want to delve further into Phaser, I have a project based course you can purchase where I guide you through the process of building a Sonic themed infinite runner game. This is a great opportunity to put in practice what you’ve learned here. If you’re interested, here’s the link to the course : https://www.patreon.com/posts/learn-phaser-4-147473030 . That said, you can freely play the game being built in the course as well as have access to the final source code. Original Phaser game live demo : https://jslegend.itch.io/sonic-ring-run-phaser-4 Demo of the version built in the course : https://jslegend.itch.io/sonic-runner-tutorial-build Final source code : https://github.com/JSLegendDev/sonic-runner-phaser-tutorial If you enjoy technical posts like this one, I recommend subscribing to not miss out on future releases. Subscribe now In the meantime, you can read the following : Phaser is the most popular JavaScript/TypeScript framework for making 2D games. It’s performant, and popular games like Vampire Survivors and PokéRogue were made with it. Because it’s a web-native framework, games made with Phaser are lightweight and generally load and run better on the web than the web exports produced by major game engines. For that reason, if you’re looking to make 2D web games, Phaser is a great addition to your toolbelt. In this post, I’ll explain the framework’s core concepts in around 5 minutes. — SPONSORED SEGMENT — In case you want to bring your web game to desktop platforms, today’s sponsor GemShell , allows you to build executables for Windows/Mac/Linux in what amounts to a click. It also makes Steam integration easy. For more info, visit 👉 https://l0om.itch.io/gemshell You have a tool/product you want featured in a sponsored segment? Contact me at [email protected] The Phaser Config A Phaser project starts with defining a config to describe how the game’s canvas should be initialized. To make the game scale according to the window’s size, we can set the scale property in our config. The mode property is set to FIT, so the canvas scales while preserving its own aspect ratio. As for keeping the canvas centered on the page, the autoCenter property is used with the CENTER_BOTH value. Scene Creation Most games are composed of multiple scenes and switching between them is expected during the course of gameplay. Since Phaser uses the object oriented paradigm, a scene is created by defining a class that inherits from the Phaser.Scene class. To be able to reference the scene elsewhere in our code, it’s important to give it a key. For this purpose and for being able to use the methods and properties of the parent class we need to call the super constructor and pass to it the key we want to use. The two most important methods of a Phaser scene are the and methods. The first is used for, among other things, creating game objects like text and sprites and setting things like scores. It runs once, every time the scene becomes active. The latter which runs once per frame is, for example, used to handle movement logic. Hooking Up a Scene to Our Game Once a scene is created, we still need to add it to our game. This is done in the Phaser config under a property called scenes, which expects an array. The order of scenes in this array is important. The first element will be used as the default scene of the game. Switching Scenes To switch scenes, we can call the method of the scene manager. Rendering Sprites Before we can render sprites, we need to load them. For this purpose, a Phaser scene has access to the method where asset loading logic should be placed. To load an image, we can use the image method of the loader plugin. Then, in the method, we can render a sprite by calling the method of the Game Object Factory plugin. The first two params are for specifying the X and Y coordinates while the third param is for providing the key of the sprite to render. Because we created our sprite game object in the method, we don’t have access to it in our method, that’s why you’ll often see the pattern of assigning a game object to an instance field so it becomes accessible to other methods of the scene. Finally, movement logic code is placed in the method which runs every frame. Rendering Text Rendering text is similar to sprites. Rather than the using the method, we use the method. Entity Creation If you want to hold data or define custom methods for a sprite game object, a better approach is to define a class that inherits from the Phaser.GameObject.Sprite class. Once the class is defined, we can use it in our scene’s code. Asset Loading While asset loading can be done in any Phaser scene, a better approach is to create a scene dedicated to loading assets, which then switches to the main game scene once loading is complete. This can be achieved like shown below : Animation API Another important aspect of any game is the ability to play animations. Usually for 2D games, we have spritesheets containing all the needed frames to animate a character in a single image. An example of a spritesheet In Phaser, we first specify the dimensions of a frame in the loading logic of the spritesheet so that the framework knows how to slice the image into individual frames. Then, we can create an animation by defining its starting and ending frames. To provide the needed frames we call the method of Phaser’s animation manager. Finally once the animation is created, it can be played by using the method of the sprite game object. If you want the animation to loop back indefinitely, add the repeat property and set it to -1. Input Handling A game needs to be interactive to be called a “game”. One way to handle input is by using event listeners provided by Phaser. For keyboard input, we can use : And for handling mouse and touch input we can use . Sharing Data Between Scenes At one point, you might need to share data between scenes. For this purpose, you can use Phaser’s registry. Here is an example of its usage. Playing Sound To play sounds (assuming you have already loaded the sound first) you can use the method of the sound manager. You can specify the sound’s volume in the second param of that method. If you need to be able to stop, pause or play the same sound at a later time, you can add it to the sound manager rather than playing it immediately. This comes in handy when you transition from one scene to another and you have a sound that loops indefinitely. In that case, you need to stop the sound before switching over otherwise the sound will keep playing in the next scene. Physics, Debug Mode, Physics Bodies and Collision Logic By default, Phaser offers an Arcade physics system which is not meant for complex physics simulations. However, it’s well suited for most types of games. To enable it, you can add the following to your Phaser config. You can add an existing game object to the physics system the same way you add one to a scene. This will create a physics body for that game object which is accessible with the body instance field. You can view this body as a hitbox around your sprite if you turn on the debug mode in your project’s config. Example of a Phaser game with debug set to true To create bodies that aren’t affected by gravity, like platforms, you can create a static group and then create and add static bodies to that group. Here’s an example : You can also add already existing physics bodies to a group. Now, you might be wondering what groups are useful for? They shine in collision handling logic. Let’s assume you have multiple enemies attacking the player. To determine when a collision occurs between any enemy and the player, you can set up the following collision handler : Project Based Tutorial There are many concepts I did not have time to cover. If you want to delve further into Phaser, I have a project based course you can purchase where I guide you through the process of building a Sonic themed infinite runner game. This is a great opportunity to put in practice what you’ve learned here. If you’re interested, here’s the link to the course : https://www.patreon.com/posts/learn-phaser-4-147473030 . That said, you can freely play the game being built in the course as well as have access to the final source code. Original Phaser game live demo : https://jslegend.itch.io/sonic-ring-run-phaser-4 Demo of the version built in the course : https://jslegend.itch.io/sonic-runner-tutorial-build Final source code : https://github.com/JSLegendDev/sonic-runner-phaser-tutorial

0 views
Lea Verou 1 weeks ago

Web dependencies are broken. Can we fix them?

Abstraction is the cornerstone of modern software engineering. Reusing logic and building higher-level solutions from lower-level building blocks is what makes all the technological wonders around us possible. Imagine if every time anyone wrote a calculator they also had to reinvent floating-point arithmetic and string encoding! In healthy ecosystems dependencies are normal, cheap, and first-class. “Dependency-free” is not a badge of honor. And yet, the web platform has outsourced this fundamental functionality to third-party tooling . As a result, code reuse has become a balancing of tradeoffs that should not have existed in the first place. In NodeJS, you just and reference specifiers straight away in your code. Same in Python, with . Same in Rust with . In healthy ecosystems you don’t ponder how or whether to use dependencies. The ecosystem assumes dependencies are normal, cheap, and first-class . You just install them, use them, and move on. “Dependency-free” is not a badge of honor. Instead, dependency management in the web platform consists of bits and bobs of scattered primitives, with no coherent end-to-end solution . Naturally, bundlers such as Webpack , rollup , and esbuild have picked up the slack, with browserify being the one that started it all, in 2012. There is nothing wrong with bundlers when used as a performance optimization to minimize waterfall effects and overhead from too many HTTP requests. You know, what a bundler is supposed to do. It is okay to require advanced tools for advanced needs , and performance optimization is generally an advanced use case. Same for most other things bundlers and build tools are used for, such as strong typing, linting, or transpiling. All of these are needs that come much later than dependency management, both in a programmer’s learning journey, as well as in a project’s development lifecycle. Dependency management is such a basic and ubiquitous need, it should be a part of the platform, decoupled from bundling. Requiring advanced tools for basic needs is a textbook usability cliff . In other ecosystems, optimizations happen (and are learned) after dependency resolution. On the web, optimization is the price of admission! This is not normal. Bundlers have become so ubiquitous that most JS developers cannot even imagine deploying code without them. READMEs are written assuming a bundler, without even mentioning the assumption. It’s just how JS is consumed. My heart breaks for the newbie trying to use a drag and drop library, only to get mysterious errors about specifiers that failed to resolve. However, bundling is not technically a necessary step of dependency management. Importing files through URLs is natively supported in every browser, via ESM imports. HTTP/2 makes importing multiple small files far more reasonable than it used to be — at least from a connection overhead perspective. You can totally get by without bundlers in a project that doesn’t use any libraries. But the moment you add that first dependency, everything changes. You are suddenly faced with a huge usability cliff : which bundler to use, how to configure it, how to deploy with it, a mountain of decisions standing between you and your goal of using that one dependency. That one drag and drop library. For newcomers, this often comes very early in their introduction to the web platform, and it can be downright overwhelming. It is technically possible to use dependencies without bundlers, today. There are a few different approaches, and — I will not sugarcoat it — they all suck . There are three questions here: There is currently no good answer to any of them, only fragile workarounds held together by duct tape. Using a dependency should not need any additional song and dance besides “install this package” + “now import it here”. That’s it. That’s the minimum necessary to declare intent . And that’s precisely how it works in NodeJS and other JS runtimes. Anything beyond that is reducing signal-to-noise ratio , especially if it needs to be done separately for every project or worse, for every dependency. You may need to have something to bite hard on while reading the next few sections. It’s going to be bad . Typically, package managers like take care of deduplicating compatible package versions and may use a directory like to install packages. In theory, one could deploy as part of their website and directly reference files in client-side JS. For example, to use Vue : It works out of the box, and is a very natural thing to try the first time you install a package and you notice . Great, right? No. Not great. First, deploying your entire directory is both wasteful , and a security risk . In fact, most serverless hosts (e.g. Netlify or Vercel ) automatically remove it from the publicly deployed files after the build is finished. Additionally, it violates encapsulation : paths within a package are generally seen as an implementation detail of the package itself, and packages expose specifier exports like or that they map to internal paths. If you decide to circumvent this and link to files directly, you now need to update your import paths whenever you update the package. It is also fragile, as not every module is installed directly in — though those explicitly marked as app dependencies are. Another common path is importing from CDNs like Unpkg and JSDelivr . For Vue, it would look like this: It’s quick and easy. Nothing to install or configure! Great, right? No. Not great. It is always a bad idea to introduce a dependency on a whole other domain you do not control , and an even worse one when linking to executable code. First, there is the obvious security risk. Unless you link to a specific version, down to the patch number and/or use SRI , the resource could turn malicious overnight under your nose if the package is compromised. And even if you link to a specific version, there is always the risk that the CDN itself could get compromised. Who remembers polyfill.io ? But even supply-chain attacks aside, any third-party domain is an unnecessary additional point of failure . I still remember scrambling to change JSDelivr URLs to Unpkg during an outage right before one of my talks, or having to hunt down all my repos that used RawGit URLs when it sunset, including many libraries. The DX is also suboptimal. You lose the immediacy and resilience of local, relative paths. Without additional tooling ( Requestly , file edits, etc.), you now need to wait for CDN roundtrips even during local development. Wanted to code on a flight? Good luck. Needed to show a live demo during a talk, over clogged conference wifi? Maybe sacrifice a goat to the gods first. And while they maintain encapsulation slightly better than raw file imports, as they let you reference a package by its name for its default export, additional specifiers (e.g. ) typically still require importing by file path. “But with public CDNs, I benefit from the resource having already been cached by another website the user visited!” Oh my sweet summer child. I hate to be the one to break it to you, but no, you don’t, and that has been the case since about 2020 . Double keyed caching obliterated this advantage . In case you were not aware, yes, your browser will redownload every single resource anew for every single website (origin) that requests it. Yes, even if it’s exactly the same. This changed to prevent cross-site leaks : malicious websites could exfiltrate information about your past network activity by measuring how long a resource took to download, and thus infer whether it was cached. Those who have looked into this problem claim that there is no other way to prevent these timing attacks other than to actually redownload the resource. No way for the browser to even fake a download by simply delaying the response. Even requiring resources to opt-in (e.g. via CORS) was ruled out, the concern being that websites could then use it as a third-party tracking mechanism. I personally have trouble accepting that such wasteful bandwidth usage was the best balance of tradeoffs for all Web users , including those in emerging economies and different locales [1] . It’s not that I don’t see the risks — it’s that I am acutely aware of the cost, a cost that is disproportionately borne by those not in the Wealthy Western Web . How likely is it that a Web user in Zimbabwe, where 1 GB of bandwidth costs 17% of the median monthly income , would choose to download React or nine weights of Roboto thousands of times to avoid seeing personalized ads? And how patronizing is it for people in California to be making this decision for them? A quick and dirty way to get local URLs for local development and CDN URLs for the remote site is to link to relative URLs, and add a URL rewrite to a CDN if that is not found. E.g. with Netlify rewrites this looks like this: Since is not deployed, this will always redirect on the remote URL, while still allowing for local URLs during development. Great, right? No. Not great. Like the mythical hydra, it solves one problem and creates two new ones. First, it still carries many of the same issues of the approaches it combines: Additionally, it introduces a new problem: the two files need to match, but the naïve approach above would always just link to the latest version. Sure, one could alleviate this by building the file with tooling, to link to specific versions, read from . But the point is not that it’s insurmountable, but that it should not be this hard . Another solution is a lightweight build script that copies either entire packages or specific exports into a directory that will actually get deployed. When dependencies are few, this can be as simple as an npm script: So now we have our own nice subset of and we don’t depend on any third-party domains. Great, right? No. Not great. Just like most other solution, this still breaks encapsulation, forcing us to maintain a separate, ad-hoc index of specifiers to file paths. Additionally, it has no awareness of the dependency graph. Dependencies of dependencies need to be copied separately. But wait a second. Did I say dependencies of dependencies? How would that even work? In addition to their individual flaws, all of the solutions above share a major flaw: they can only handle importing dependency-free packages . But what happens if the package you’re importing also uses dependencies? It gets unimaginably worse my friend, that’s what happens. There is no reasonable way for a library author to link to dependencies without excluding certain consumer workflows. There is no local URL a library author can use to reliably link to dependencies, and CDN URLs are highly problematic. Specifiers are the only way here. So the moment you include a dependency that uses dependencies, you’re forced into specifier-based dependency management workflows , whether these are bundlers, or import map flavored JSON vomit in every single HTML page (discussed later). As a fig leaf, libraries will often provide a “browser” bundle that consumers can import instead of their normal , which does not use specifiers. This combines all their dependencies into a single dependency-free file that you can import from a browser. This means they can use whatever dependencies they want, and you can still import that bundle using regular ESM imports in a browser, sans bundler. Great, right? No. Not great. It’s called a bundle for a reason. It bundles all their dependencies too, and now they cannot be shared with any other dependency in your tree, even if it’s exactly the same version of exactly the same package. You’re not avoiding bundling, you’re outsourcing it , and multiplying the size of your JS code in the process. And if the library author has not done that, you’re stuck with little to do, besides a CDN that rewrites specifiers on the fly like esm.sh , with all CDN downsides described above. As someone who regularly releases open source packages ( some with billions of npm installs ), I find this incredibly frustrating. I want to write packages that can be consumed by people using or not using bundlers, without penalizing either group , but the only way to do that today is to basically not use any dependencies. I cannot even modularize my own packages without running into this! This doesn’t scale. Browsers can import specifiers, as long as the mapping to a URL is explicitly provided through an import map . Import maps look like this: Did you notice something? Yes, this is an HTML block. No, I cannot link to an import map that lives in a separate file. Instead, I have to include the darn thing in. Every. Single. Page. The moment you decide to use JS dependencies, you now need an HTML templating tool as well. 🙃 “💡 Oh I know, I’ll generate this from my library via DOM methods! ” I hear you say. No, my sweet summer child. It needs to be present at parse time. So unless you’re willing to it (please don’t), the answer is a big flat NOPE. “💡 Ok, at least I’ll keep it short by routing everything through a CDN or the same local folder ” No, my sweet summer child. Go to sleep and dream of globs and URLPatterns . Then wake up and get to work, because you actually need to specify. Every. Single. Mapping. Yes, transitive dependencies too. You wanted to use dependencies? You will pay with your blood, sweat, and tears. Or, well, another build tool. So now I need a build tool to manage the import map , like JSPM . It also needs to talk to my HTML templating tool, which I now had to add so it can spit out these import maps on. Every. Single. HTML. Page. There are three invariants that import maps violate: Plus, you still have all of the issues discussed above, because you still need URLs to link to. By trying to solve your problem with import maps, you now got multiple problems. To sum up, in their current form, import maps don’t eliminate bundlers — they recreate them in JSON form, while adding an HTML dependency and worse latency. Given the current state of the ecosystem, not using bundlers in any nontrivial application does seem like an exercise in masochism. Indeed, per State of JS 2024 , bundlers were extremely popular, with Webpack having been used by 9 in 10 developers and having close to 100% awareness! But sorting by sentiment paints a different picture, with satisfaction, interest, and positivity dropping year after year. Even those who never question the status quo can feel it in their gut that this is not okay. This is not a reasonable way to manage dependencies. This is not a healthy ecosystem. Out of curiosity, I also ran two polls on my own social media. Obviously, this suffers from selection bias , due to the snowball sampling nature of social media, but I was still surprised to see such a high percentage of bundle-less JS workflows: I’m very curious how these folks manage the problems discussed here. Oftentimes when discussing these issues, I get the question “but other languages are completely compiled, why is it a problem here?”. Yes, but their compiler is official and always there. You literally can’t use the language without it. The problem is not compilation, it’s fragmentation. It’s the experience of linking to a package via a browser import only to see errors about specifiers. It’s adding mountains of config and complexity to use a utility function. It’s having no clear path to write a package that uses another package, even if both are yours. Abstraction itself is not something to outsource to third-party tools. This is the programming equivalent of privatizing fundamental infrastructure — roads, law enforcement, healthcare — systems that work precisely because everyone can rely on them being there. Like boiling frogs , JS developers have resigned themselves to immense levels of complexity and gruntwork as simply how things are . The rise of AI introduced swaths of less technical folks to web development and their overwhelm and confusion is forcing us to take a long hard look at the current shape of the ecosystem — and it’s not pretty. Few things must always be part of a language’s standard library, but dependency management is absolutely one of them. Any cognitive overhead should be going into deciding which library to use, not whether to include it and how . This is also actively harming web platform architecture . Because bundlers are so ubiquitous, we have ended up designing the platform around them, when it should be the opposite. For example, because is unreliable when bundlers are used, components have no robust way to link to other resources (styles, images, icons, etc.) relative to themselves, unless these resources can be part of the module tree. So now we are adding features to the web platform that break any reasonable assumption about what HTML, CSS, and JS are, like JS imports for CSS and HTML, which could have been a simple if web platform features could be relied on. And because using dependencies is nontrivial, we are adding features to the standard library that could have been userland or even browser-provided dependencies. To reiterate, the problem isn’t that bundlers exist — it’s that they are the only viable way to get first-class dependency management on the web. JS developers deserve better. The web platform deserves better. As a web standards person, my first thought when spotting such a lacking is “how can the web platform improve?”. And after four years in the TAG , I cannot shake the holistic architectural perspective of “which part of the Web stack is best suited for this?” Before we can fix this, we need to understand why it is the way it is. What is the fundamental reason the JS ecosystem overwhelmingly prefers specifiers over URLs? On the surface, people often quote syntax, but that seems to be a red herring. There is little DX advantage of (a specifier) over (a URL), or even (which can be configured to have a JS MIME type). Another oft-cited reason is immutability: Remote URLs can change, whereas specifiers cannot. This also appears to be a red herring: local URLs can be just as immutable as specifiers. Digging deeper, it seems that the more fundamental reason has to do with purview . A URL is largely the same everywhere, whereas can resolve to different things depending on context. A specifier is app-controlled whereas a URL is not. There needs to be a standard location for a dependency to be located and referenced from, and that needs to be app-controlled. Additionally, specifiers are universal . Once a package is installed, it can be imported from anywhere, without having to work out paths. The closest HTTP URLs can get to this is root-relative URLs, and that’s still not quite the same. Specifiers are clearly the path of least resistance here, so the low hanging fruit would be to make it easier to map specifiers to URLs, starting by improving import maps. An area with huge room for improvement here is import maps . Both making it easier to generate and include import maps, and making the import maps themselves smaller, leaner, and easier to maintain. The biggest need here is external import maps , even if it’s only via . This would eliminate the dependency on HTML templating and opens the way for generating them with a simple build tool. This was actually part of the original import map work , and was removed from the spec due to lack of implementer interest, despite overwhelming demand. In 2022, external import maps were prototyped in WebKit (Safari), which prompted a new WHATWG issue . Unfortunately, it appears that progress has since stalled once more. External import maps do alleviate some of the core pain points, but are still globally managed in HTML, which hinders composability and requires heavier tooling. What if import maps could be imported into JS code? If JS could import import maps, (e.g. via ), this would eliminate the dependency on HTML altogether, allowing for scripts to localize their own import info, and for the graph to be progressively composed instead of globally managed. Going further, import maps via an HTTP header (e.g. ) would even allow webhosts to generate them for you and send them down the wire completely transparently. This could be the final missing piece for making dependencies truly first-class. Imagine a future where you just install packages and use specifiers without setting anything up, without compiling any files into other files, with the server transparently handling the mapping ! However, import maps need URLs to map specifiers to, so we also need some way to deploy the relevant subset of to public-facing URLs, as deploying the entire directory is not a viable option. One solution might be a way to explicitly mark dependencies as client side , possibly even specific exports. This would decouple detection from processing app files: in complex apps it can be managed via tooling, and in simple apps it could even be authored manually, since it would only include top-level dependencies. Even if we had better ways to mark which dependencies are client-side and map specifiers to URLs, these are still pieces of the puzzle, not the entire puzzle. Without a way to figure out what depends on what, transitive dependencies will still need to be managed globally at the top level, defeating any hope of a tooling-light workflow. The current system relies on reading and parsing thousands of files to build the dependency graph. This is reasonable for a JS runtime where the cost of file reads is negligible, but not for a browser where HTTP roundtrips are costly. And even if it were, this does not account for any tree-shaking. Think of how this works when using URLs: modules simply link to other URLs and the graph is progressively composed through these requests. What if specifiers could work the same way? What if we could look up and route specifiers when they are actually imported? Here’s a radical idea: What if specifiers were just another type of URL , and specifier resolution could be handled by the server in the same way a URL is resolved when it is requested? They could use a protocol, that can be omitted in certain contexts, such as ESM imports. How would these URLs be different than regular local URLs? Architecturally, this has several advantages: Obviously, this is just a loose strawman at this point, and would need a lot of work to turn into an actual proposal (which I’d be happy to help out with, with funding ), but I suspect we need some way to bridge the gap between these two fundamentally different ways to import modules. Too radical? Quite likely. But abstraction is foundational, and you often need radical solutions to fix foundational problems. Even if this is not the right path, I doubt incremental improvements can get us out of this mess for good. But in the end, this is about the problem . I’m much more confident that the problem needs solving, than I am of any particular solution. Hopefully, after reading this, so are you. So this is a call to action for the community. To browser vendors, to standards groups, to individual developers. Let’s fix this! 💪🏼 Thanks to Jordan Harband , Wes Todd , and Anne van Kesteren for reviewing earlier versions of this draft. In fact, when I was in the TAG, Sangwhan Moon and I drafted a Finding on the topic, but the TAG never reached consensus on it. ↩︎ Use specifiers or URLs? How to resolve specifiers to URLs? Which URL do my dependencies live at? Linking to CDNs is inherently insecure It breaks encapsulation of the dependencies Locality : Dependency declarations live in HTML, not JS. Libraries cannot declare their own dependencies. Composability : Import maps do not compose across dependencies and require global coordination Scalability : Mapping every transitive dependency is not viable without tooling Twitter/X poll : 17.6% of respondents Mastodon poll : 40% (!) of respondents Their protocol would be implied in certain contexts — that would be how we can import bare specifiers in ESM Their resolution would be customizable (e.g. through import maps, or even regular URL rewrites) Despite looking like absolute URLs, their resolution would depend on the request’s header (thus allowing different modules to use different versions of the same dependency). A request to a URL without an header would fail. HTTP caching would work differently; basically in a way that emulates the current behavior of the JS module cache. It bridges the gap between specifiers and URLs . Rather than having two entirely separate primitives for linking to a resource, it makes specifiers a high-level primitive and URLs the low-level primitive that explains it. It allows retrofitting specifiers into parts of the platform that were not designed for them, such as CSS . This is not theoretical: I was at a session at TPAC where bringing specifiers to CSS was discussed. With this, every part of the platform that takes URLs can now utilize specifiers, it would just need to specify the protocol explicitly. In fact, when I was in the TAG, Sangwhan Moon and I drafted a Finding on the topic, but the TAG never reached consensus on it. ↩︎

0 views
Manuel Moreale 1 weeks ago

How Do You Read My Content

Recently, Kev posted a survey on his site to figure out how people access his content. Big fan of asking people directly and the results are not at all surprising to me. As I said to him, RSS traffic on my server is VERY high. But it's fun to get more datapoints so I created a similar survey and I'd really appreciate it if you could take probably 10 seconds to answer it. It's literally 1 question. I'll keep the form live for a week and then publish the results. Thank you :) Thank you for keeping RSS alive. You're awesome. Email me :: Sign my guestbook :: Support for 1$/month :: See my generous supporters :: Subscribe to People and Blogs

0 views
Manuel Moreale 1 weeks ago

Yearly reminder to use RSS

The year is 2026, and RSS is still, by far, the best way to keep up with sites on the web. If you already know what RSS is but you’re not currently using it, consider this a reminder for you to dust off that RSS reader of yours and put it back to use. And don’t listen to the party-poopers that claim that RSS is dead. It is not. If instead you don’t know what RSS is, here’s a very brief explanation: RSS is a technology that allows you to create your own personal feed, using an RSS reader app, where content from different sources is aggregated and displayed—usually—in reverse chronological order. The same way you use a browser to access my site, you can use an RSS reader app to access the RSS feeds available on my website. Keep in mind that not all sites have RSS feeds available. It used to be the norm, but then the web got enshittified. I wrote a longer post about RSS years ago , but the web is full of resources if you want to get into RSS. And you should, because RSS is awesome. So go get an RSS reader app , stop being spoon-fed slop by algorithmic platforms, and start consuming content at your own pace. Thank you for keeping RSS alive. You're awesome. Email me :: Sign my guestbook :: Support for 1$/month :: See my generous supporters :: Subscribe to People and Blogs

0 views
Jim Nielsen 1 weeks ago

Creating “Edit” Links That Open Plain-Text Source Files in a Native App

The setup for my notes blog looks like this : I try to catch spelling issues and what not before I publish, but I’m not perfect. I can proofread a draft as much as I want, but nothing helps me catch errors better than hitting publish and re-reading what I just published on my website. If that fails, kind readers will often reach out and say “Hey, I found a typo in your post [link].” To fix these errors, I will: However, the “Open iA Writer” and “Find the post” are points of friction I’ve wanted to optimize. I’ve found myself thinking: “When I’m reading a post on and I spot a mistake, I wish I could just click an ‘Edit’ link right there and be editing my file.” You might be thinking, “Yeah that’s what a hosted CMS does.” But I like my plain-text files. And I love my native writing app. What’s one to do? Well, turns out iA Writer supports opening files via links with this protocol: So, in my case, I can create a link for each post on my website that will open the corresponding plain-text file in iA Writer, e.g. And voilà, my OS is now my CMS! It’s not a link to open the post in a hosted CMS somewhere. It’s a link to open a file on the device I’m using — cool! My new workflow looks like this: It works great. Here’s an example of opening a post from the browser on my laptop: And another on my phone: Granted, these “Edit” links are only useful to me. So I don’t put them in the source markup. Instead, I generate them with JavaScript when it’s just me browsing. How do I know it’s just me? I wrote a little script that watches for the presence of a search param . If that is present, my site generates an “Edit” link on every post with the correct and stores that piece of state in localstorage so every time I revisit the site, the “Edit” links are rendered for me (but nobody else sees them). Well, not nobody. Now that I revealed my secret I know you can go get the “Edit” links to appear. But they won’t work for you because A) you don’t have iA Writer installed, or B) you don’t have my files on your device. So here’s a little tip if you tried rendering the “Edit” links: do to turn them back off :) Reply via: Email · Mastodon · Bluesky Content is plain-text markdown files (synced via Dropbox, editable in iA Writer on my Mac, iPad, or iPhone) Codebase is on GitHub Builds are triggered in Netlify by a Shortcut Open iA Writer Find the post Fire Shortcut to trigger a build Refresh my website and see the updated post Read a post in the browser Click “Edit” hyperlink to open plain-text file in native app Make changes Fire Shortcut to trigger a build

0 views
The Jolly Teapot 1 weeks ago

New year, new me, new web browsing setup?

Since we’re at the start of a new year, I will stop fine-tuning everything on this blog and let it live as the receptacle it’s supposed to be. With my mind cleared of HTML and CSS concerns, I now have energy to waste on new optimisations of my digital environment, and this time with an old favourite of mine: content blockers. * 1 In 2022, I experimented with blocking JavaScript on a per-site basis , which, at the time, allowed me to feel better about my behaviour on the web. You see, I thought that I was not actively refusing adverts. I was just disabling a specific technology on my web browser; not my fault if most ads are enabled via JS after all. True, ads couldn’t reach my house, but not because I actively refused their delivery; simply because the trucks used for their delivery weren’t allowed to drive on my pedestrian-only street. Ethically, I preferred this approach to the one blocking all ads blindly on every site, even if the consequences, from the publishers’ perspective, were the same. I know it was very hypocritical of me, and I know I was still technically blocking the ads. Nevertheless, I felt less guilty blocking the technology used for ads, and not the ads directly. This setup was fine, until it wasn’t. My web experience was not great. Blocking JavaScript by default breaks too many non-media sites, and letting it on made me realise how awful browsing the web without a content blocker can be. The only way for this system to work was to have patience and discipline on the per-site settings. Eventually, I gave up and reinstalled the excellent Wipr Safari extension on all my devices a few weeks later. Last year, on top of Wipr , I also tried services like NextDNS and Mullvad DNS . With these, the browser ad blocker becomes almost superfluous, as all it has to do is remove empty boxes that were supposed to be ads before being blocked by the DNS. It was an efficient setup, but I was still blocking ads, which kept on bothering me. While I happily support financially a few publications, I can’t do the same for all the sites I visit. For the ones I am not paying, seeing ads seems like a fair deal; blocking ads was making me feel increasingly guilty. * 2 Like I wrote in the other post on the topic : Somehow, I always feel a little bit of shame and guilt when talking about content blockers, especially ad blockers. Obviously ads are too often the only way many publishers manage to make decent money on the internet: every newspaper can’t be financially successful with subscriptions, and every media company can’t survive only on contributions and grants. That’s why recently, I stopped using Mullvad as my DNS resolver, and switched to Quad9 , which focuses on privacy-protection and not ad-blocking. I also uninstalled Wipr. Today, I rely solely on StopTheScript . What’s new this time around is that I will try to be more disciplined than I was three years ago, and do the work to make this system last. What I do is set the default StopTheScript setting on “Ask”. When a site aggressively welcomes me with three or four banners masking the article I came to read, I click on the StopTheScript icon and allow it to block JavaScript on the website, and refresh the page. Two clicks, one keyboard shortcut. In most cases, these steps are easier and faster than what is the usual series of events. You know, the one where you need to reload the page with ad blockers disabled, just so you can close the modal window that was blocking scrolling on the page, and then reload the page once again, this time with ad blockers enabled. With JavaScript turned off, visiting most websites is a breeze: my computer feels like it uses an M4 chip and not an Intel Core i5, the page is clean, the article is there, it works. There are a few media sites that refuse to display anything with JS turned off, but I’d say that 95% of the time it’s fine, and I can live my life without a proper ad blocker. * 3 For websites where ads are tolerable, I don’t bother blocking JavaScript, I let it pass. In my mind, this is how my first interaction with a website goes if it were a department store: [opens page at URL] Website: “ Hi dear visitor, I see you’re looking at this product, but may I interest you in a free newsletter? Or would you like to share your Google account with us so next time you come back we’ll know? Also, could you sign this agreement real quick? Oh, and by the way, have you seen that we have a special offer currently? Would you like a cookie? ” Me: “ Hello, yes, oh wow, hum… wait a second… ” [blocks JavaScript] Me: “ Sorry, I don’t speak your language and don’t understand anything you say .” [Salesperson goes away instantly] Me: “ Ah, this is nice and quiet. ” Maybe I’m wrong, but to me, this is a more “polite” default behaviour than using an ad blocker from the get-go, which, in this analogy, would be something like this: [opens page at URL] Ad blocker: “ Alright, well done team, great job. We arrested all sales people, handcuffed them, and brought them all to in the basement. All clear. The boss can come in. ” Me: “ Ah, this is nice and quiet. ” If you have a better analogy, I’m all ears: I really struggled with this one. I’m not sure how long this JS blocking setup will last this time. I’m not sure if it feels that much better to block JS permanently on some websites rather than blocking ads. All I know is that most websites are much quicker to load without JavaScript, much easier to handle by my machine, and just for those reasons, StopTheScript may be the best content blocker for Safari. I guess this is not surprising that all the cool new web browsers include a JavaScript toggle natively. Why are they called content blockers and not ad blockers? Pretty sure it’s some sort of diplomatic lingo used to avoid hurting the feelings of ad companies. I don’t like the word content , but calling ads and trackers content is just weird. ^ I know I could use an ad blocker and disable it on some websites, or only activate it on the most annoying sites, but ad blockers tend to disappear in the background, don’t they? ^ I mention media sites because obviously ecommerce sites, video sites, and interactive sites require JavaScript. Interestingly, Mastodon doesn’t need it to display posts, whereas Bluesky does. ^ Why are they called content blockers and not ad blockers? Pretty sure it’s some sort of diplomatic lingo used to avoid hurting the feelings of ad companies. I don’t like the word content , but calling ads and trackers content is just weird. ^ I know I could use an ad blocker and disable it on some websites, or only activate it on the most annoying sites, but ad blockers tend to disappear in the background, don’t they? ^ I mention media sites because obviously ecommerce sites, video sites, and interactive sites require JavaScript. Interestingly, Mastodon doesn’t need it to display posts, whereas Bluesky does. ^

0 views
Rob Zolkos 1 weeks ago

A Month Exploring Fizzy

In their book Getting Real , 37signals talk about Open Doors — the idea that you should give customers access to their data through RSS feeds and APIs. Let them get their information when they want it, how they want it. Open up and good things happen. Fizzy takes that seriously. When 37signals released Fizzy with its full git history available , they didn’t just open-source the code — they shipped a complete API and webhook system too. The doors were wide open baby! So I dove in — reading the source, building tools, and sharing what I found. Every time curiosity kicked in, there was a direct path from “I wonder if…” to something I could actually try and execute. This post is a catch-all for my very bubbly month of December. Fizzy Webhooks: What You Need to Know — I set up a local webhook receiver to capture and document every event type Fizzy sends. The post covers the payload structures, signature verification, and ideas for what you could build on top of the webhook system. The Making of Fizzy, Told by Git — I prompted Claude Code to analyze the entire git history and write a documentary about the development. Vanilla CSS is all you need — Diving into the no-build CSS architecture across Campfire, Writebook, and Fizzy. Fizzy Design Evolution: A Flipbook from Git — I went through each day of commits, got the application to a bootable state, seeded the database, and took a screenshot. Then I stitched those screenshots into a flipbook video with a soundtrack made from Fizzy’s own audio files. Fizzy’s Pull Requests: Who Built What and How — An analysis of who owned which domains in the Fizzy codebase. The post maps contributors to their expertise areas and curates learning paths through the PRs for topics like Turbo/Hotwire, caching, AI integration, multi-tenancy, and webhooks. The open API invited experimentation. I spotted gaps that would make integration easier for other developers, so I filled them: fizzy-api-client — Ruby client for the Fizzy API. fizzy-client-python — Python client for the Fizzy API. fizzy-cli — Command-line interface for the Fizzy API, built first in Ruby and then migrated to Go for portability. fizzy-skill — An AI agent skill for interacting with Fizzy. n8n-nodes-fizzy — An n8n community node that brings Fizzy into your automation workflows. Create cards, manage assignments, and react to real-time events through webhook triggers. Migration tools — I built these to make it easier to try Fizzy without starting from scratch. Migrating your existing issues and boards gives you an immediate sense of how it could work for you, without having to manually create test cards. You can see your real data running in Fizzy from day one, which I think makes it easier to evaluate and decide if its useful for you. I also contributed a few small fixes back to the main repository: Fizzy is released under the O’Saasy License , which is similar in spirit to MIT but includes a restriction on offering the software as a competing hosted or SaaS product. You can modify and self-host it, but you can’t repackage it and sell it as your own hosted service. I built O’Saasy Directory to make it easy to find applications released under this license. Beyond Fizzy, the directory includes other submitted projects where the source is available to read and modify. If you have built something under the O’Saasy License, visit the submission page to add yours. Having built the Fizzy CLI and fizzy-api-client Rubygem, I saw some fun opportunities to build little lab experiments to show how Fizzy could be integrated with - both to power up some functionality that isn’t there yet, but also creating boards in some interesting ways (eg Movie Quiz). I got the idea for this on a flight to Australia with no internet. Just a pad of paper and a pen. I should probably do that more often as a bunch of ideas for all sorts of products came out. CarbonationLabs is not a product per se. It’s an open source Rails application designed to be run locally where you can interact with the hosted or self-hosted versions of Fizzy. If anything I hope it inspires creation of little problem solving workflows for Fizzy that wouldn’t be built into the main product (the problem is too niche). The API and webhook system is really flexible and most of your bespoke problems could be solved with some creative thinking. Introducing Carbonation Labs - fun ways to add experiments to and extend Fizzy (repo link and demo videos below)🧵 I built carbonation.dev to bring together all the tools, libraries, and integrations that I and others in the community have created for Fizzy. It’s a directory covering API clients (Ruby, Python, JavaScript), CLI tools with packages for macOS, Arch Linux, Debian, Fedora, and Windows, integrations for Claude Code and other AI agents, n8n, Raycast, Telegram, and MCP servers, plus migration tools for GitHub, Linear, Asana, and Jira. If you’ve built something for Fizzy, I’d love to feature it. You can submit a pull request to add your tool to the directory. Building the Fizzy CLI pushed me into some new territory. I created an AUR package for Arch Linux users, set up a Homebrew tap for macOS, published my first Python package to PyPI, and made an n8n plugin — all firsts for me. While I already knew Go, rewriting the CLI in it was a fun exercise, and building TUIs for the setup and skill commands introduced me to terminal UI libraries I hadn’t used before. Gosh it was fun! If you want to get better at Rails, Fizzy is a great place to study real-world code. And in my view if you want to work at 37signals as a Rails programmer, digging into Fizzy — along with Campfire and Writebook — is a solid way to learn how they approach Rails architecture and design decisions. Submitting PRs is also a good way to contribute back while learning — just be respectful of the contribution policy . The review discussions give you a window into how to reason about problems, spot opportunities, and make trade-offs. This month pushed parts of my creative thinking that weren’t gone, but definitely weren’t being stressed. Like any muscle, use it or lose it. The direction of what to explore came from my own curiosity and a habit of poking around under the hood, and AI helped me move a lot faster once I knew where I wanted to go. Most of this information already exists somewhere — Google, Stack Overflow, documentation — but having AI right there alongside me as a partner was thrilling. All of this was made possible because a team left the doors open. No one asked me to step inside; I decided to invest the time and do the work to see what I could build, learn and share. I do this at work too—when I can—looking for opportunities I can shape, experiment with, and get genuinely excited about. Most importantly I had fun and I hope you enjoyed following along. linear2fizzy — Migrate Linear issues jira2fizzy — Migrate JIRA issues asana2fizzy — Migrate Asana tasks gh2fizzy — Migrate GitHub Issues prd2fizzy — Convert PRDs to Fizzy cards #2114 — Remove unused install.svg and its CSS class #2111 — Remove unpaired view-transition-name #2095 — Fix typo: minues → minutes #2094 — Fix duplicate word: use use → use #2093 — Add QrCodesController test #2088 — Fix view-transition-name typo in public card show

0 views
Danny McClelland 1 weeks ago

Scheduled Deploys for Future Posts

One of the small joys of running a static blog is scheduling posts in advance. Write a few pieces when inspiration strikes, set future dates, and let them publish themselves while you’re busy with other things. There’s just one problem: static sites don’t work that way out of the box. With a dynamic CMS like WordPress, scheduling is built in. The server checks the current time, compares it to your post’s publish date, and serves it up when the moment arrives. Simple. Static site generators like Hugo work differently. When you build the site, Hugo looks at all your content, checks which posts have dates in the past, and generates HTML for those. Future-dated posts get skipped entirely. They don’t exist in the built output. This means if you write a post today with tomorrow’s date, it won’t appear until you rebuild the site tomorrow. And if you’re using Netlify’s automatic deploys from Git, that rebuild only happens when you push a commit. No commit, no deploy, no post. I could set a reminder to push an empty commit every morning. But that defeats the purpose of scheduling posts in the first place. The fix is straightforward: trigger a Netlify build automatically every day, whether or not there’s new code to deploy. Netlify provides build hooks for exactly this purpose. A build hook is a unique URL that triggers a new deploy when you send a POST request to it. All you need is something to call that URL on a schedule. GitHub Actions handles the scheduling side. A simple workflow with a cron trigger runs every day at midnight UK time and pings the build hook. Netlify does the rest. First, create a build hook in Netlify: Next, add that URL as a secret in your GitHub repository: Finally, create a workflow file at : The dual cron schedule handles UK daylight saving time. During winter (GMT), the first schedule fires at midnight. During summer (BST), the second one does. There’s a brief overlap during the DST transitions where both might run, but an extra deploy is harmless. The trigger is optional but handy. It adds a “Run workflow” button in the GitHub Actions UI, letting you trigger a deploy manually without pushing a commit. Now every morning at 00:01, GitHub Actions wakes up, pokes the Netlify build hook, and a fresh deploy rolls out. Any posts with today’s date appear automatically. No manual intervention required. It’s a small piece of automation, but it removes just enough friction to make scheduling posts actually practical. Write when you want, publish when you planned. Go to your site’s dashboard Navigate to Site settings → Build & deploy → Build hooks Click Add build hook , give it a name, and select your production branch Copy the generated URL Go to Settings → Secrets and variables → Actions Create a new repository secret called Paste the build hook URL as the value

0 views
Simon Willison 1 weeks ago

Introducing gisthost.github.io

I am a huge fan of gistpreview.github.io , the site by Leon Huang that lets you append to see a browser-rendered version of an HTML page that you have saved to a Gist. The last commit was ten years ago and I needed a couple of small changes so I've forked it and deployed an updated version at gisthost.github.io . The genius thing about is that it's a core piece of GitHub infrastructure, hosted and cost-covered entirely by GitHub, that wasn't built with any involvement from GitHub at all. To understand how it works we need to first talk about Gists. Any file hosted in a GitHub Gist can be accessed via a direct URL that looks like this: That URL is served with a few key HTTP headers: These ensure that every file is treated by browsers as plan text, so HTML file will not be rendered even by older browsers that attempt to guess the content type based on the content. These confirm that the file is sever via GitHub's caching CDN, which means I don't feel guilty about linking to them for potentially high traffic scenarios. This is my favorite HTTP header! It means I can hit these files with a call from any domain on the internet, which is fantastic for building HTML tools that do useful things with content hosted in a Gist. The one big catch is that Content-Type header. It means you can't use a Gist to serve HTML files that people can view. That's where comes in. The site belongs to the dedicated gistpreview GitHub organization, and is served out of the github.com/gistpreview/gistpreview.github.io repository by GitHub Pages. It's not much code. The key functionality is this snippet of JavaScript from main.js : This chain of promises fetches the Gist content from the GitHub API, finds the section of that JSON corresponding to the requested file name and then outputs it to the page like this: This is smart. Injecting the content using would fail to execute inline scripts. Using causes the browser to treat the HTML as if it was directly part of the parent page. That's pretty much the whole trick! Read the Gist ID from the query string, fetch the content via the JSON API and it into the page. Here's a demo: https://gistpreview.github.io/?d168778e8e62f65886000f3f314d63e3 I forked to add two new features: I also removed some dependencies (jQuery and Bootstrap and an old polyfill) and inlined the JavaScript into a single index.html file . The Substack issue was small but frustrating. If you email out a link to a page via Substack it modifies the URL to look like this: https://gistpreview.github.io/?f40971b693024fbe984a68b73cc283d2=&utm_source=substack&utm_medium=email This breaks because it treats as the Gist ID. The fix is to read everything up to that equals sign. I submitted a PR for that back in November. The second issue around truncated files was reported against my claude-code-transcripts project a few days ago. That project provides a CLI tool for exporting HTML rendered versions of Claude Code sessions. It includes a option which uses the CLI tool to publish the resulting HTML to a Gist and returns a gistpreview URL that the user can share. These exports can get pretty big, and some of the resulting HTML was past the size limit of what comes back from the Gist API. As of claude-code-transcripts 0.5 the option now publishes to gisthost.github.io instead, fixing both bugs. Here's the Claude Code transcript that refactored Gist Host to remove those dependencies, which I published to Gist Host using the following command: You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . A workaround for Substack mangling the URLs The ability to serve larger files that get truncated in the JSON API

0 views
Allen Pike 2 weeks ago

A Box of Many Inputs

One of the interesting questions when designing AI-enabled software is, “What does search input mean?” This was once a simple question: if a user entered “squish” in a search box, it would of course return things that contained “squish”. Over time though, computers have improved to the point where a kind of universal “omnibox” is now possible. Today software can: Ideally, users shouldn’t need to think about which mode they want. They should just be able to type enough characters to make their intent clear, then press Enter. While this is great in theory, it creates a very crowded design space for input boxes. While most kinds of software will end up with an omnibox-like input – especially with the rise of universal command bars – one of the most fiendish cases is found in the web browser. At birth, browsers’ address bar required precise input. To get to Wikipedia, you would have had to type precisely. Now, you can mangle it as and still get there, which is impressive in its own right. However, with modern technology there is even more we can do with the address bar. The rise of “AI browsers” like Dia, Atlas, and Comet all strive to layer more functionality into this input field. For example, in Dia if you type in the address bar, it will forego Google and use the in-browser chat to correctly answer $610 million. If it sees a question, it sends it to chat. Seems maybe helpful, right? Except sometimes you’re not asking a question, you’re searching for a question. Check out what happens when you search for the movie “Who Framed Roger Rabbit?” below. This is basically never what the user wants when they type a film title into the browser omnibox. Now, if you typed “who framed roger rabbit” into ChatGPT, a chat response would not be surprising. It’s a chat app. But people expect browsers to be able to browse to things, even if the thing’s name starts with . Admittedly, Dia’s algorithm for question detection is more complex than prefix-matching. They’ve bundled a little classifier that detects if a phrase has “question vibes”. By doing question classification locally with a very small model 1 , they can identify questions within milliseconds, with no effect on privacy. Smart! Except. There are a lot of cases where it’s difficult to classify a phrase by whether it should get an answer or a search result. Consider these results from Dia’s question classifier, all of which should do a search: So it’s imperfect at identifying questions. But more critically, even if they hill-climbed to 100% on “is this a question” accuracy, that’s still a dead end. The actual classification we want is “is this a question they are asking , or is it a phrase they want to search for ?” Unfortunately, that’s a much harder training problem! Currently, it’s beyond the ken of a fast local classifier. As I’ve been weighing alternative browsers, I expected other “AI browser” contenders to have the same problem. To my surprise, no. ChatGPT Atlas avoids it entirely by keeping their heuristic very simple: if a query has less than 10 words, send it to search – otherwise chat. 3 If you want to explicitly send a short query to chat, you can press ⌘ + return. Meanwhile, Perplexity’s Comet and Google’s Chrome send every query to their respective search engines, which use larger server-side models to determine whether a given result should get a more chat-forward response or a more results-centric one. As of today, Google errs on the side of returning web results, and Perplexity errs on the side of giving an AI-generated answer, but both can do either. In time, these different search and chat engines will converge toward a design that gives users a good experience almost every time, without the need for modes. Already Google is testing out merging their “AI mode” into the AI overviews that appear on results. And presumably OpenAI will evolve to give more search-like responses to pedestrian search queries, in their ever-growing quest for world domination. Of the four leading contenders for the “AI browser” crown, only Dia tries to interpret as a question. From here, Dia can go one of two ways. They can either go all-in on their own answer engine, competing with Perplexity and Google, so they can send every query there. Or, they can concede on the local classifier and do as ChatGPT Atlas has done – just route all short queries to search. Earlier this month, Atlassian CEO and Dia acquirer Mike Cannon-Brookes gave an interview where he mentioned the acquisition and the future vision . Reading the tea leaves, it seems Dia could fulfill Atlassian’s vision of building the browser for professional productivity without needing to become a full-on search engine. On the other hand, Cannon-Brookes mentioned the supposedly-retired Arc 4 browser three times and didn’t use the phrase Dia once. So… it’s hard to tell exactly what the path is going to look like. Regardless, with The Browser Company’s talent and Atlassian’s reach, it would be rash to count them out in this one. They’ll fix the Roger Rabbit thing soon enough. But while browsers encountered it first, time will bring more and more ambitious text boxes. Many of them will initially struggle to deliver on our intent. But, despite some growing pains, the holy grail is within reach. People should be able to type what they want their software to do, and it should do it. And it should do so without any unpleasant surprises. Quickly. It won’t be easy! But it’ll be great. You can see in the app bundle that it’s a DistilBERT model, with LoRA adapters for routing search queries and for identifying sensitive content, running on Apple’s MLX for speed. It looks like the whole local ML package is about 160MB. While that size class of model can be powerful for certain tasks, it’s hard to pack in enough world knowledge to distinguish between film titles and popular quotes vs. genuine questions.  ↩ I love the idea of the model thinking, “Oh I found a question! starts with ! So the user is asking whether or not ‘of beans’ can recipe.”  ↩ Dia also sends long queries to chat – anything over 12 words.  ↩ I can’t mention Arc without a shout out to one of my favourite features of all time: Tab Tidy. If you have a bunch of tabs open in Arc’s vertical tab bar, a little Tidy button will group the tabs by logical topic or task. Then, you can close them group by group. So neat.  ↩ Return things with your input’s typo fixed Return things with a synonym to your input Autocomplete recent or popular things Directly retrieve an answer to your input Jump to an action based on your input Generate a conversational response to your input You can see in the app bundle that it’s a DistilBERT model, with LoRA adapters for routing search queries and for identifying sensitive content, running on Apple’s MLX for speed. It looks like the whole local ML package is about 160MB. While that size class of model can be powerful for certain tasks, it’s hard to pack in enough world knowledge to distinguish between film titles and popular quotes vs. genuine questions.  ↩ I love the idea of the model thinking, “Oh I found a question! starts with ! So the user is asking whether or not ‘of beans’ can recipe.”  ↩ Dia also sends long queries to chat – anything over 12 words.  ↩ I can’t mention Arc without a shout out to one of my favourite features of all time: Tab Tidy. If you have a bunch of tabs open in Arc’s vertical tab bar, a little Tidy button will group the tabs by logical topic or task. Then, you can close them group by group. So neat.  ↩

0 views
Grumpy Gamer 2 weeks ago

Sqlite Comments

When I started using Hugu for static site generation I lost the ability to have comments and we all know now supportive the Internet can be, so why wouldn’t you have comments? I wrote a few php scripts that I added on to Hugo and I had comments again. I decided to store the comments as flat files so I didn’t complicate things by needing the bloated MySQL. I wanted to keep it as simple and fast as possible. When a comment is added, my PHP script created a directory (if needed) for the post and saves the comment out as a .json file with name as the current time to make sorting easy. When the blog page was displayed, these files (already sorted thanks to the filename) were loaded and displayed. And it all worked well until it didn’t. Flat files are simple. but they can be hard to search or maintain if they need cleaning up or dealt with after a spam attack. I figured I use commandline tools to do all of that, but it’s a lot more cumbersome than I first thought. I missed have them in a sql database. I didn’t want to install MySQL again, but my site doesn’t get a lot of commenting traffic so I could use Sqlite instead. The downside is Sqlite write-locks the database while a write is happening. In my case it’s a fraction of a second and wouldn’t be a issue. The second problem I had was the version of Ubuntu my server was using is 5 years old and some of the packages I wanted wouldn’t available for it. I tried to update Ubuntu and for reasons I don’t fully understand I couldn’t. So I spun up a new server. Since grumpygamer.com is a statics site I only had to install Apache and I was off and running. Fun times. But the comment flat files still bugged me and I thought I’d use this as an opportunity to convert over to Sqlite. PHP/Apache comes with Sqilte already installed, so that’s easy. A long weekend and I rewrote the code to save comments and everything is back and working. Given that a webserver and PHP already needed to be installed, it isn’t a big deal to use Sqlite. If you’re not comfortable with SQL, it might be harder but I like SQL.

0 views