Default Browser Switching

Raycast

Part of the Raycast series

As you saw from my Default Apps December 2023 post, I use Safari for personal use and Chrome for web work and for some administrative and server-related tools that work best in it. The downside to this is that links go to the default browser from things like 1Password, Fastmarks, and email messages, for example. This means when I’m using Chrome, what I really need is for it to be my default browser, and the rest of the time, Safari to be.

Because I’m a Raycast user, I was intrigued by the Raycast Script Commands GitHub repo. The script command examples in this repo include system commands that include default browser scripts for Arc, Chrome, Chromium, Firefox, and Safari.

Raycast Script Commands

Raycast script commands are basically scripts that are registered in Raycast and have hooks that let it interact with them to pass parameters and show output in Raycast, if desired. These can be Bash scripts, AppleScript, Swift, Python, Ruby, or Node.js scripts.

Raycast Create Script Command menu

Raycast Default Browser Script Commands

The default browser scripts in the Raycast script command repo rely on a very short Objective-C program that you compile on your Mac called defaultbrowser, which lets you change your default browser from the command line. Given this, you might be surprised to learn that these script commands are AppleScript, and not Bash scripts. The reason is simple: buttons.

When you tell your Mac to switch default browsers using defaultbrowser, you are presented with a dialog box giving you the option to set whatever browser you wanted as your new default, or to keep whatever browser is the current default. Bash scripts can’t click buttons, but AppleScript can. It can also run Bash scripts, which lets us call defaultbrowser from within the AppleScript.

Here’s the default-browser-safari.applescript script from the Raycast script commands repo:

#!/usr/bin/osascript
# Dependency: requires defaultbrowser (https://github.com/kerma/defaultbrowser)
# Install via Homebrew: `brew install defaultbrowser`
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title Default to Safari
# @raycast.mode silent
# @raycast.packageName Browser
# Optional parameters:
# @raycast.icon images/safari.png
# Documentation:
# @raycast.author Marcos Sánchez-Dehesa
# @raycast.authorURL https://github.com/dehesa
# @raycast.description Set Safari as the default browser.
set repeatCount to 0
tell application "System Events"
try
my changeDefaultBrowser("safari")
repeat until button 2 of window 1 of process "CoreServicesUIAgent" exists
delay 0.01
set repeatCount to repeatCount + 1
if repeatCount = 15 then exit repeat
end repeat
try
click button 2 of window 1 of process "CoreServicesUIAgent"
log "Safari is now your default browser"
on error
log "Safari is already your default browser"
end try
on error
log "The \"defaultbrowser\" CLI tool is required: https://github.com/kerma/defaultbrowser

The top half is information for Raycast. Then the AppleScript portion gets going. First it calls a function1 that runs the defaultbrowser command line program with the string safari as a parameter value. Then it runs a loop waiting for the confirmation dialog box to pop up. It waits until either the window exists or it’s looped 15 times. Finally, it tries to click button 2 of the dialog box. The reason it tries to click button 2 is because the dialog looks like the following image – or at least, it’s supposed to. More on that later.

Default browser change confirmation dialog box

This means when you send it a request to change your default browser from Safari to Chrome, for example, you’ll get a dialog box with two buttons, the first of which cancels the change and the second of which executes the change to Chrome.

I created new Raycast script commands using the appropriately named “Create Script Command” command in Raycast, and then copied the AppleScripts from the repo into them. Hooray! End of blog post, right?

Except there was a problem.

Chrome Does It Differently

Everything worked great whenever I tried switching from any browser that wasn’t Chrome to any other browser. But whenever I tried switching FROM Chrome back to something else, it never worked.

A simple test using defaultbrowser directly in the command line showed me why. Chrome’s confirmation dialog box is different. It looks like this:

Dialog box when switching from Chrome to a different default browser

You see the problem. The command scripts assume that button 2 is the button for making the switch, but in the case of Chrome’s dialog box, it’s the other way around. Button 1 makes the change and button 2 keeps the current default browser setting.

I honestly don’t know whose responsibility this is, Google’s or Apple’s, and I don’t really care. I presume it’s Google’s because I presume at one point the Raycast versions of these script commands worked, even when switching away from Chrome as the default browser, so my guess is Google changed something about Chrome’s confirmation dialog. I don’t know. Honestly, I would have thought this was entirely handled by macOS.

Modifying the Default Browser Scripts to Handle the Chrome Dialog Box

The solution is simple: if switching from Chrome, click button 1. Otherwise, click button 2. Since the script could be switching to a non-Chrome browser from a different non-Chrome browser, this means I need to check. And that sent me down a rabbit hole that I refuse to admit the time duration of, because it was long enough to need to add an “s” to “hour”.

But it goes like this:

On macOS, your default browser setting is one of many settings saved in com.apple.launchservices.secure.plist in your local Library/Prefences folder (~/Library/Preferences). You can search for it. If you type the following, you’ll get a LONG output that includes two lines you care about:

Terminal window
plutil -p ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist

Those two lines are:

Terminal window
"LSHandlerRoleAll" => "com.apple.safari"
"LSHandlerURLScheme" => "https"

That’s if Safari is your current default browser, of course. It could be org.mozilla.firefox or com.google.chrome, for example. But you want to search for a line with LSHandlerRoleAll set to some browser, followed by a line called LSHandlerURLScheme set to https.

Fortunately, awk was made for things like this, and also fortunately, AppleScript can run shell commands. So I made another function for my default browser AppleScripts to see what the current browser is.

to getCurrentDefaultBrowser()
set filePath to "~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
set output to do shell script "plutil -p " & filePath & " | awk '/LSHandlerRoleAll/{a=$3}/LSHandlerURLScheme/{if($3==\"\\\"https\\\"\") print a}'"
return output
end getCurrentDefaultBrowser

Now, before running the part of the script that runs defaultbrowser before waiting for a button to click, it checks which browser is the current default browser. If it’s com.google.chrome, then my script clicks button 1 for me. Otherwise it clicks button 2.

#!/usr/bin/osascript
# Dependency: requires defaultbrowser (https://github.com/kerma/defaultbrowser)
# Install via Homebrew: `brew install defaultbrowser`
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title Set Default Safari
# @raycast.mode silent
# @raycast.packageName Browser
# Optional parameters:
# @raycast.icon images/safari.png
# Documentation:
# @raycast.author scott_willsey
# @raycast.authorURL https://raycast.com/scott_willsey
set currentDefaultBrowser to my getCurrentDefaultBrowser()
set repeatCount to 0
tell application "System Events"
try
my changeDefaultBrowser()
repeat until button 2 of window 1 of process "CoreServicesUIAgent" exists
delay 0.01
set repeatCount to repeatCount + 1
if repeatCount = 15 then exit repeat
end repeat
try
if currentDefaultBrowser contains "com.google.chrome" then
click button 1 of window 1 of process "CoreServicesUIAgent"
else
click button 2 of window 1 of process "CoreServicesUIAgent"
end if
log "Safari is now your default browser"
on error
log "Safari is already your default browser"
end try
on error
log "The \"defaultbrowser\" CLI tool is required: https://github.com/kerma/defaultbrowser"
end try
end tell
to getCurrentDefaultBrowser()
set filePath to "~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
set output to do shell script "plutil -p " & filePath & " | awk '/LSHandlerRoleAll/{a=$3}/LSHandlerURLScheme/{if($3==\"\\\"https\\\"\") print a}'"
return output
end getCurrentDefaultBrowser
to changeDefaultBrowser()
do shell script "
if ! command -v defaultbrowser &> /dev/null; then
exit 1
fi
defaultbrowser " & "safari" & "
exit 0
"
end changeDefaultBrowser

Now when I have Chrome set as my default browser and I don’t want that anymore, my Raycast default browser command scripts work as intended, and will actually manage to set my default browser to the desired one.

A Parameterized Version of the Default Browser Command Script

Just in case anyone else cares, I also created a parameterized version of the script that lets me type in the browser name as a raycast command parameter so that the same script can switch to any browser I want.

Parameterized default browser script command

#!/usr/bin/osascript
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title Set Default Browser
# @raycast.mode silent
# Optional parameters:
# @raycast.icon images/Safari.png
# @raycast.argument1 { "type": "text", "placeholder": "Browser Name" }
# Documentation:
# @raycast.author scott_willsey
# @raycast.authorURL https://raycast.com/scott_willsey
on run argv
# check for null or empty item 1 of argv
if (item 1 of argv) is "" then
log "You must enter a browser name."
else
set currentDefaultBrowser to my getCurrentDefaultBrowser()
set browserName to item 1 of argv
set repeatCount to 0
tell application "System Events"
try
my changeDefaultBrowser(browserName)
repeat until button 2 of window 1 of process "CoreServicesUIAgent" exists
delay 0.01
set repeatCount to repeatCount + 1
if repeatCount = 15 then exit repeat
end repeat
try
if currentDefaultBrowser contains "com.google.chrome" then
click button 1 of window 1 of process "CoreServicesUIAgent"
else
click button 2 of window 1 of process "CoreServicesUIAgent"
end if
log browserName & " is now your default browser"
on error
log browserName & " is already your default browser"
end try
on error
log "The \"defaultbrowser\" CLI tool is required: https://github.com/kerma/defaultbrowser"
end try
end tell
end if
end run
to getCurrentDefaultBrowser()
set filePath to "~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
set output to do shell script "plutil -p " & filePath & " | awk '/LSHandlerRoleAll/{a=$3}/LSHandlerURLScheme/{if($3==\"\\\"https\\\"\") print a}'"
return output
end getCurrentDefaultBrowser
to changeDefaultBrowser(browserName)
do shell script "
if ! command -v /opt/homebrew/bin/defaultbrowser &> /dev/null; then
exit 1
fi
defaultbrowser " & browserName & "
exit 0
"
end changeDefaultBrowser

Footnotes

  1. Custom handler in AppleScript parlance, apparently

Default Apps December 2023

Once upon a time,1 the Hemispheric Views podcast held the first ever Duel of the Defaults in which they decided who was the winner at using the most default apps for several categories. Subsequently, Gabz posted his own default app list, and then Robb Knight took it to 11 with his App Defaults page.

In the spirit of this new fashion of blogging about one’s default apps, here are mine as of December, 2023.

NOTE: These are my personal use apps. They have nothing to do with my job at Monolith 3000, which is an all-Microsoft shop.

Official Categories

  • 📮 Mail Server
  • 📨 Mail Client
    • Apple Mail
  • 📝 Notes
  • To-Do
    • Reminders App
  • 📷 iPhone Photo Shooting
    • iOS Camera
  • 🟦 Photo Management
    • Photos App
  • 📅 Calendar
    • Apple Calendar
  • 📂 Cloud File Storage
    • iCloud Drive
  • 📖 RSS
  • 👩‍🦲 Contacts
    • Apple Contacts App
  • 🌐 Browser
    • Safari, Chrome for web work
  • 💬 Chat
    • Messages, Signal, Discord, Slack
  • 🔖 Bookmarks
  • 📑 Read It Later
  • 📜 Word Processing
    • Pages
    • Google Docs
    • Microsoft Word
  • 📈 Spreadsheets
    • Excel (I’m sorry, but Numbers is the most useless excuse for a spreadsheet app I’ve ever seen)
  • 📊 Presentations
    • Keynote
  • 🛒 Shopping List
    • Reminders
  • 🍴 Meal Planning
  • 💰 Budgeting and Personal Finance
    • Spreadsheet
  • 📰 News
    • RSS
    • Mastodon
    • Apple News
  • 🎵 Music
    • Apple Music
  • 🎤 Podcasts
  • 🔐 Password Management

Additional Categories

Footnotes

  1. November 2nd, 2023, in fact

Warp Blocks

Part of the Warp series

I like Warp. I did not intend to like Warp. I didn’t even want to like Warp. But after giving it a fair shot for a few days, I couldn’t help but liking it and sticking with it. It just feels to me like everything a next generation terminal should be.

According to its developers,

Warp is a modern, Rust-based terminal with AI built in so you and your team can build great software faster.

That sounds nice, if not a bit vague. What is so great about Warp, and what makes it more desirable to use than macOS Terminal app or iTerm2 or Secure Shellfish, for example? The obvious answer is its features and the workflows they enable. One of my favorite of those features is Blocks.

Blocks

Blocks are one of the biggest Warp features, in my opinion, and one that I love and take advantage of all the time. Blocks are simply a grouping of a command and its output into one selectable and actionable section in Warp. For example, here’s the result of a ps -aux command that is now a block.

Warp Block for a process list command

My new waiting command prompt is below the block and is now in a separate block from it, but I can scroll up and down to see the whole ps -aux block and perform actions on it. Blocks are nice because not only do they negate having to use something like ps -aux | more to see the full output of a command, but they also remain selectable, filterable, and modifiable.

Speaking of filterable… here’s that same block filtered by the word “python”. Look how small it is… down from 124 lines of output to 3!

Filtering can accept regular expressions or case-sensitive text simply by choosing those options in the filter search, just like most modern text editors. The keyboard shortcut for filtering is Shift-Option-F (⇧ ⌥ F).

You may be thinking, hang on… I can just pipe the command to grep like this: ps -aux | grep 'python', and you’re right. You can. But filtering is fast and you can further act on it by saving the filtered output or the filtered output and the command, and more.

Saving the filtered output of a Block

Update 2023-12-17

Warp has a great blog post about Block filtering vs. grep that help answer the obvious question of why use block filtering instead of just using grep: New utilities for your terminal, inspired by text editors | Warp

Bookmarking blocks is nice too. For example, here I left the previous ps -aux block filtered for “python” and bookmarked it. I then executed a bunch of other commands which all created their own blocks and the bookmarked ps -aux block scrolled off the screen. But by hitting Option-up (⌥-↑), I immediately jumped back up to the bookmarked (and still filtered) block.

Jumping to a bookmarked Block

Filtering isn’t the only way to find things in a block, you can also use standard old Command-F (⌘-F) to search within a block. The difference between this and filtering is exactly what the words imply: filtering reduces the output of the command to just lines with matches, while search finds search text or regular expression matches in the command output, but doesn’t filter to only show matching lines.

Searching in a Block

One nice feature is using search in a “live” block, like one that’s running an htop or a tail -f command. As the output of the running process updates, so do your search results.

There’s more to Warp Blocks than what I’ve mentioned here. Check out Warp’s documentation on Blocks to see for yourself.

Astro 3.3 Picture Component

Part of the Responsive Images series

The last time I wrote about image optimization and responsive images in Astro, there were some compromises to be made in order to use astro:assets image optimization.

First, it did not resize images or create responsive source sets, it only optimized the format and file byte size with the same width and height. Second, it did and still does require importing images in a way that at the very least requires you to know the file extension of the original, and in a way that I thought made dynamically creating image source file names impossible. And finally, there was no control at all over how images referenced in markdown are optimized.

The good news is that as of Astro 3.3, there is an improved astro:assets Image component as well as a new astro:assets Picture component that solve a lot of my problems. The issue of leaving it up to Astro to do what it wants with markdown images still exists, but I have a compromise for that issue.1 In the meantime, Astro 3.3+ can now create multiple image formats, multiple image pixel sizes, and can create both img and picture tags with source sets for great image responsiveness.

A Problem Requiring Responsive Images and Dynamic Image Names

Let’s look at a real world use case to show you a typical scenario where you need to create your image source file names dynamically as well as create multiple sizes.

Friends with Brews is a podcast about conversations and different kinds of brewed drinks – beer, coffee, or tea. Because part of the show’s premise is trying new drinks and giving them a thumbs-up or thumbs-down, I also want to keep a list of these drinks and showcase them both on the individual episodes they’re consumed on as well as on an aggregate brews page.

Friends with Brews brew list page

Each of these little detail blocks links to a details page for the specific drink.

Drink details page

Because several of these images are typically displayed on any given page, or because the image on the individual drink detail pages are dimensionally larger, I want them optimized as much as possible. In addition, I want them to be as responsive as possible for different size screens and different pixel densities.

One thing to note is that all of the drink details for every drink are kept in a JSON file called brews.json. This JSON file includes the name of the image associated with the drink. The JSON entry for a specific brew looks like this:

{
"id": "YodfV6yTeB75PJpmQDPgU",
"name": "Oktoberfest Märzen",
"brewery": "Hacker-Pschorr",
"image": "HackerPschorrOktoberfestMarzen-8E6221D0-7EB7-46B3-8AFA-5CA4A1E6AC1B",
"description": "As it was forbidden to brew in summer, a stronger beer -- the Märzen -- was brewed earlier in March. It would finally be served at the Oktoberfest, under the \"Heaven of Bavaria.\" We have returned once again to the age-old recipe and recreated that gloriously smooth, honey-coloured piece of history from times gone by. Perfect with: Bavarian sausage salad and all the titbits a true Bavarian would also enjoy: roast chicken, sausages or suckling pig cooked in Wiesn beer.",
"type": "beer",
"sortOrder": "0",
"episodes": ["54"],
"url": "https://www.hacker-pschorr.com/our-beers/usa/oktoberfest-maerzen",
"rating": [
{
"host": "Peter",
"vote": "thumbs-up",
"description": "It is malty, it is toasty, and it is crisp. Very happy, this is good."
}
]
},

Note the image entry: the image name doesn’t have a file extension for technical reasons, namely that when you’re importing images in an Astro file, you can’t create a dynamic file name including the file extension. Vite has to know the file extension ahead of time, even if you do cobble together the rest of the file name at compile time.

Because of this, I just made all my images PNG, regardless of what they would later be optimized to by the various image optimization components I’ve used over the life of the Friends with Brews website, and therefore I also didn’t include the file extension because I would be hard-coding it into the image name string to keep Vite happy.

It sounds more confusing than it is. Here’s my code pre-Astro 3.0, back when Astro had an image optimization tool called @astrojs/image which had both Image and Picture components. Note the src property of the Picture component and the above paragraph will make more sense.

---
import { Icon } from "astro-icon/components";
import { Picture } from "@astrojs/image/components";
const { brew, episode } = Astro.props;
---
<div class="brew">
<div class="brew-image">
<a href={`/bottle/${brew.id}/`}>
<Picture
src={`/images/brews/${brew.image}.png`}
widths={[400, 800, 1200]}
aspectRatio="1:1"
sizes="200px"
formats={["avif", "webp", "png"]}
alt={`${brew.brewery} ${brew.name}`}
/>
</a>
</div>

Astro 3.0 - 3.2.x Solution

After migrating to Astro 3 and before Astro 3.3 gave the ability to generate multiple widths for each desired image format, I was generating those myself. For my brew images, I was creating 400 pixel wide and 1000 pixel wide images for each drink in addition to the original size. This was all automated with Retrobatch, but it was still a little more work and management on my part.

Retrobatch automated image generation

In my case, I would generate the 400 and 1000 pixel wide images in webp, and also keep the full-sized png on hand to link to for people who like staring at large images with equally impressive download times.

Here’s what my BrewsDetails.astro file looked like with this solution:

---
import { Icon } from "astro-icon/components";
import { capitalize, brewIcon } from "./utilities/stringformatter.mjs";
const { brew, episode } = Astro.props;
const brewImage = brew.image.replace(/(-)(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})$/, "-400$1$2") + ".webp";
---
<div class="brew">
<div class="brew-image">
<a href={`/bottle/${brew.id}/`}>
<img src={`/images/brews/${brewImage}`} alt={brew.name} />
</a>
</div>
<!-- Etc, etc, etc -->

The first thing eagle-eyed readers will notice is that I’m not using any kind of image component at all here – just a plain old html img tag. This is because I was already generating the optimized images and because I was using a wider image size than needed to help with the high pixel-density display cases.

This string is just making sure we’re grabbing the 400 pixel wide version of the image for this view:

const brewImage = brew.image.replace(/(-)(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})$/, "-400$1$2") + ".webp";

This is all fine and dandy, but it’s less optimal than the pre-Astro 3.0 days, because the html I was using does nothing for lazy loading, does nothing for only grabbing the largest image needed for the device screen size and pixel density, and is basically a massive compromise.

I was not a giant fan of having to do this, in other words.

Fortunately, the smart people at Astro kept refining Astro 3.x’s image optimization solution.

Astro 3.3+ Solution

Now that Astro 3.3+ has the ability to use the Image or Picture components to generate multiple widths per image format, this solves the problem of different screen sizes and pixel densities. Now I can let it generate the extra images for me.

Here’s what the Picture component looks like for these small drink images:

---
import { Icon } from "astro-icon/components";
import { Picture } from "astro:assets";
import { capitalize, brewIcon } from "./utilities/stringformatter.mjs";
const { brew, episode } = Astro.props;
---
<div class="brew">
<div class="brew-image">
<a href={`/bottle/${brew.id}/`}>
<Picture src={import(`../assets/images/brews/${brew.image}.png`)}
formats={['avif', 'webp', 'jpg']}
width="400"
widths={[400, 800]}
alt={`${brew.brewery} ${brew.name}`} />
</a>
</div>
<!-- Etc, etc, etc -->

Here’s the file diff between my Astro 3.0-3.2 version versus my new Astro 3.3+ version:

BrewsDetails.astro file diff

By the way, remind me to talk about Kaleidoscope sometime. Its ability to diff things in multiple views, act as the command line difftool, and show diffs between different git commit versions is really spectacular.

Importing images using dynamic file names

Notice the difference between the way the Picture src property is composed here compared to the pre-Astro 3 version in the first half of this article? This is because the images located in the src directory need to be imported for the astro:assets Image and Picture components. Typically import statements do not like dynamically constructed strings – they error out and complain that you need to give them a static string.

Fortunately I ran across an Astro Discord thread explaining how to do the import using a dynamic string (still requiring the file extension to be statically defined, of course), and let’s just melodramatically say that it changed my life.

Another equivalent way to do it would be to use import.meta.glob to grab all the image assets and then grab the image using the key of the matching image file name, like this:

---
const images = import.meta.glob("../../assets/images/brews/*.png");
---
<Picture src={images[`../../assets/images/brews/${bottle.image}.png`]()} formats={['avif', 'webp']} alt="A description." />`

If, like me, you were desperately hoping for a better way to optimize your images after migrating to Astro 3, your prayers are pretty much answered. There’s still the issue of image links in your markdown files, though. I’ll cover that topic in a future post.

Resources

Astro 3.3: Picture component | Astro Blog
Images Component 🚀 Astro Documentation
Picture Component 🚀 Astro Documentation
My Responsive Images series

Footnotes

  1. The issue of how markdown images are optimized is not completely immaterial because markdown image links are used for every single image inside blog posts or podcast show notes, for example.

Podcast Transcripts With Audio Hijack

Part of the My Podcasting Workflows series

There I was sitting in bed after a long work day, reading SixColors, as one should, when I stumbled across this article: Audio Hijack adds automatic transcription.

This is kind of exciting. I have mixed feelings about it, actually. On the one hand, not having to laboriously create transcripts after the fact with the admittedly lovely MacWhisper does sound good. On the other hand, I’ll still have to edit the transcript to more closely mirror the edited podcast, since this will transcribe the live recorded version of the conversation instead of the edited version.

Anyway, who cares – obviously I’m going to try this out for the next recording of Friends with Brews and see how it goes. Here’s my setup, which I believe should let me get an attributed transcription:

Audio Hijack Transcript Block

I’ve used MacWhisper’s Transcribe Podcast feature on the last two episodes of Friends with Brews I’ve transcribed to get speaker attribution. You can see it in action on the transcription for Episode 52 – If I Were a Beer. If I can replicate that while recording the podcast instead of as a separate task later, you can bet that Audio Hijack Transcribe block will be value added.

Podcast Recording Practices

Part of the My Podcasting Workflows series

I talked previously about the recording software I use for podcasting on the Mac. It’s all great stuff, and if you haven’t read that article, you should. Even if you think you’re not a podcaster, maybe someday you will be, and each of those applications has use far beyond creating podcasts.

At some point in the near-ish future, I want to talk about specific app setups and use methodologies for podcasting, but before I do that I want to talk about some general best practices for recording. Some of these will help make the life of whoever edits your podcast a lot easier, assuming they care about sound quality and conversation flow.

I know a lot of people say no one cares about audio quality or crosstalk or words repeated three times in a row, but people do care. It’s a lot different being in a conversation and not really hearing all the starts and stops and repeated words and uhms and ahhs, but they start to matter for people listening to podcasts.

Since your goal is to make your podcast as enjoyable as possible and you don’t know what annoys any given listener, why not make a good faith effort to make it as listenable as possible? In terms of recording, it takes very little extra effort to have a good quality sound and smooth conversation versus one that makes it obvious you couldn’t be bothered.1

My motto about effort is “I can care less than you do”, meaning that if you, the media or product producer, don’t even care enough to try, why should anyone else care about your product either? This is not the same as saying you have to sound like This American Life or have custom jingles and the best artwork in existence. It does mean you need to clearly be trying so that your listeners know you really do care about their experience. Other people won’t care about your thing unless you care about your thing.

Get Rid of Noise

If you have a noisy podcast, some segment of your audience, probably a much larger segment than you think, will notice and will care. There have been several podcasts I’ve tried in the past that I just couldn’t listen to because it was too obvious that the hosts just recorded without testing or listening back to their own material and threw it out there for the world to suffer to.

When I say noisy, I’m using the term as a catch-all for background noise, hiss in the recording, water heaters, air conditioning, vocal reverb or extreme compression (think Skype or Zoom recordings), and even things like clipping, which is when your signal is too loud for the mic to handle and the waveform clips at the top, resulting in a very painful noise in the recording. It’s amazing how many people don’t even bother to see if they’re clipping or not, and it can be hard to fix in post.

Some simple steps for getting rid of noise in your podcast include the following.

Consider Your Mic

Use a decent mic, preferably one that doesn’t pick up every sound between the mic diaphragm and Mars. For this reason, condenser mics like the oddly popular Blue Yeti will not work for many people.

Echoing Is for the Alps

Reduce echo in your recording environment. Foam panels and temporary throw rugs and curtains can do wonders to absorb sound rather than let it bounce around.

Don’t Blow Hot (Or Cold) Air

Record as far away from air blowers as you can. Sometimes you’ll have no choice but to record with AC or heating on – let your editor know (maybe that’s you) so they can test some recordings of yours and see if they can remove the noise cleanly. Chances are they can, but they may need a little trial and error.

Mute Yourself

Have a mute switch, be it hardware or software based. I have an Elgato Wave XLR, which has a capacitive touch mute switch, and I love it. Peter uses an app called Shush since he’s using a USB mic.

Use your mute switch when you cough, snort, drink (unless your podcast IS at least partially about the drink), eat, or otherwise make noises that your editor should care about. Also mute whenever not talking to avoid the temptation to talk over someone else, which also makes editing take longer (again, your editor should care about crosstalk).

It’s All in the Technique

Learn good mic technique. This is harder than it sounds. It’s very easy to talk very loud sometimes and very quietly at other times. Some mics require eating the mic and others will overdrive in a second if you’re an inch too close and be at a nice level otherwise. Stay at a consistent distance and position relative to the mic, and don’t forget you ARE recording and not just having a nice zoom call.

What you’re striving for are consistent levels and quality of voice, and both can change drastically based on your own speech habits (talking loudly at the start of a sentence and trailing off into a whisper) or distance from mic. Practice and listen to your practice recordings.

Think Global, Act Local

Have everyone record their own mic locally. You will put each of these recordings into your editor and line them up. This will give you the ability to eliminate crosstalk (which you can’t do with everyone other than you on one track), get better, non-compressed sound quality (which you can’t do with a Skype or Zoom recording), compensate for noise and volume differences between other hosts (which is much harder to do with everyone else on one track), and can and should result in the best sounding voices for everyone.

Making everyone sound as good as possible is the editor’s goal. If you’re a host and also the editor, you get zero points for caring about the sound of your own voice and no one else’s.

There are lots of other considerations for clean sound, but this is a start, and I’ve already yammered on for almost 950 words now. So just one more thing:

Practice and Tweak

This does not mean I want you to be a tweaker, but I do want you to record and listen to the recordings. Have your co-hosts do the same, and give everyone’s test recordings to the editor to play with. Change your environment and/or recording settings as necessary. This takes practice, googling, and research if you’re not an audio engineer, but fortunately there is enough good quality affordable hardware and software that can create a nice sound that you don’t need to become an audio engineer either.

Next time I’ll talk about some specific software settings I use to try to get a decent recording. I know this post is very high-level and theoretical, but it’s really more of a nudge to think about these things and realize they need to be taken into consideration. Your listeners will thank you, but so will your inner producer. It’s your name on the product, so why not make it as pleasant to listen to as possible?

Footnotes

  1. This probably sounds preachy, and I don’t claim to have the most amazing podcast production quality in the world. But I do TRY. The listeners can give me feedback if they think I’m failing at it, but if anyone says I don’t take it seriously, they are wildly incorrect.

Astro 3 and Responsive Images

Part of the Responsive Images series

Astro 3 is here and you’re looking at a site published with it. I spent a few days last week converting this site and Friends with Brews from Astro 2.x to Astro 3, and there are some things I had to learn and decide to make it happen, and most of it has to do with responsive images.

You may recall my long saga of responsive image generation methods for this site and for Friends with Brews. Most of this journey has revolved around the fact that there are two kinds of optimized images on my sites:

  • Site related (logos, images for permanent site pages, etc),
  • Content related (images in blog posts or podcast episode show notes).

Site related images are easy. I can use whatever the image optimization scheme de jour that Astro uses because these are known, unchanging files and because I refer to them inside Astro components.

In the case of Astro 3, the astro:assets Image component is the official way to create optimized images inside Astro components.

This component is a lot less flexible and capable than the previous @astrojs/image component. There’s no Picture component, it doesn’t resize images from the original size, and as a result it’s easy to wind up with larger image sizes than you need and not have smaller physical sizes for responsive views like phone browsers. But it handles Astro SSR mode and prevents CLS (cumulative layout shift), which is really annoying to see.

I have relatively few site images on either this site or Friends with Brews – mostly site logos and pictures of the offenders (me on this site and Peter and me on Friends with Brews).

Using Image in an Astro component involves importing the images to be used and setting those imported images as the Image component src.

---
import { Icon } from "astro-icon/components";
import { Image } from "astro:assets";
import Base from "../layouts/Base.astro";
import site from "../data/site.json"
import av from "../assets/images/ScottLatest.jpg";
let title = "About Me - " + site.title;
let description = "About Scott Willsey";
---
<Base title={title} description={description}>
<article>
<h1>About Me</h1>
<Image
class="about-av"
src={av}
width={600}
format={"webp"}
alt="Scott Willsey"
quality={85}
/>
(etc, etc, etc)
</Base>

It’s important to note again that width={600} doesn’t result in Image creating a 600 px wide version of the image. It will create a webp image of 85% quality as specified but in the original image dimensions and aspect ratio. The 600 pixel width is used in the HTML that it outputs to tell the browser what to do with it:

<article data-astro-cid-kh7btl4r>
<h1 data-astro-cid-kh7btl4r>About Me</h1>
<img src="/_astro/ScottLatest.69c944ff_Ctgce.webp" class="about-av" alt="Scott Willsey" data-astro-cid-kh7btl4r width="600" height="600" loading="lazy" decoding="async">
<p data-astro-cid-kh7btl4r>Hello, friend.</p>
(etc, etc)
</article>

Content related images are images IN the content of the site itself, whether that be blog posts (this site) or podcast episode show notes (Friends with Brews). In my case, my content is in Markdown, so my image links are just standard Markdown image links. They are also hyperlinked to the original full-sized image.

The one really interesting setting is "Keep History For", which has options for 7 days, 30 days, 3 months, 6 months, 1 year, or unlimited. Unlimited comes with a warning that your hard drive will slowly be eaten alive and you should be ok with that.
[![Clipboard History retention length setting](../../assets/images/posts/ClipboardHistoryLengthSetting-E511BEDE-4432-49AB-A442-05069F910E41.png)](/images/posts/ClipboardHistoryLengthSetting-E511BEDE-4432-49AB-A442-05069F910E41.png)
The Clipboard History view itself is relatively simple. You can have pinned history items, and below those are your history items in reverse chronological order. The beauty of a clipboard history function is that you can copy several things in a row from a source and then worry about pasting them all later without losing any of them.

Get rid of all that hyperlink stuff and the Markdown for the image itself looks like this:

![Clipboard History retention length setting](../../assets/images/posts/ClipboardHistoryLengthSetting-E511BEDE-4432-49AB-A442-05069F910E41.png)

Tangent:

Yes, I put UUIDs in my image names so that I never have to worry about name collisions, however unlikely. I have this automated on my Mac with folder actions. I talked about folder actions in my post about Automatic Image Processing With AppleScript and Retrobatch, but basically the idea is you can set a script to run whenever a specific thing happens to a folder. I can dump a bunch of images into a specific folder and they get output into another folder with the original file name plus a UUID appended.

Anyway, the good news is that the astro:assets Image component handles images in Markdown for you by creating optimized versions in more efficient file types. Here you can see one specific png that it created a webp version for, and the difference in file size.

Terminal window
scott@Songoku:scottwillsey3.0 main 2d cd dist/_astro
scott@Songoku:_astro main 2d ll FolderActionsSetupMenu*
-rw-r--r--@ 1 scott staff 527607 Sep 15 18:37 FolderActionsSetupMenu-86B4A871-966E-4A27-A2C5-3FC85E131D6C.4464166b.png
-rw-r--r--@ 1 scott staff 60900 Sep 15 18:37 FolderActionsSetupMenu-86B4A871-966E-4A27-A2C5-3FC85E131D6C.4464166b_Q8a7k.webp
scott@Songoku:_astro main 2d

One thing that isn’t different is the image dimensions:

Image component optimized vs. original image dimensions

This means I have to pay more attention to original image sizes that I’d prefer to. I’m hoping in the future this new Images component will gain some of the functionality back that it had before, but my guess is the real monkey in the wrench in doing some of those things is the SSR support.

Right now I have a Folder Action that resizes my images to a maximum width of 1770 pixels. I can use those for the blog post inline images and link to the original sized ones. That gives me a compromise between image size and responsiveness, taking into account the high-resolution nature of most modern screens. Images need to be 2x and in many cases 3x the size they’ll be shown at in order to look sharp.

At some point I’ll probably revisit this but no matter what I do, astro:assets Image currently will not generate image srcset HTML for me, so even if I generate multiple sizes for the same file format, only one of them is ever going to get used.

Markdown vs MDX

You may recall that when I originally converted the site from Eleventy to Astro, I used MDX for my content files instead of Markdown. MDX allows the use of components and would let me directly use the astro:assets Image component in my content for content images the same way I do in my Astro components for site images.

Astro 3 will let you use the astro:assets Image component directly in MDX. This gives you more control over the HTML that’s generated for your image, but it also comes with extra complexity in terms of the writing process.

I decided a long time ago that I don’t want to go down that path any longer because when I’m writing, I don’t want to mix implementation details with content. I don’t write my blog posts in a code editor, and when I’m writing, I don’t want to have to remember how I have everything set up, and I don’t want to have to import components and images into a content file. I just want to use Markdown links.

Image Component vs Astro Remark Eleventy Image

The last Astro 2.x version of this site used Astro Remark Eleventy Image to generate responsive images for images inside markdown content. It worked well, it’s very flexible, and it had several upsides. The downsides are no support for SSR, if you need that, and it results in a slower build process because it doesn’t cache previously rendered images. Because of this latter fact, I decided to move entirely to the astro:assets Image component.

I was also worried about Astro’s Image component trying to process images in my Markdown files and fighting Astro Remark Eleventy Image, but I haven’t tested to see if this will happen. I may make a branch and link to my public images folder for images in Markdown and see if that avoids a collision between the components. If so, I may wind up using Astro Remark Eleventy Image again to get more responsive images with better file size/quality compromises.

Conclusion

Even when I’m done, I’m never done. That’s the real takeaway here. As with anything, careful consideration of how you use images and what the compromises are between different solutions is required with Astro 3 (and every version of every web framework, honestly). I’m sure there will be much more on the topic of image optimization from me in the future, given my history with it so far.