Sometimes, something you think should be simple turns out not to be. I’d assumed that it would be simple to create a file with a URL in it and then when it was double clicked would open the browser to the URL. This is partly true but not completely. For the gotchas read on.
The webloc format
You CAN simply create a file with a URL in it and opening it will open that URL in your default browser. This file is called a webloc file and the contents look like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>URL</key>
<string>https://spokenlikeageek.com</string>
</dict>
</plist>
Important Note!
Yes, I know you can create a webloc file by dragging and dropping the URL from the address bar on the browser but where’s the fun in that?
Solution #1
From the command line you can run the following script every time you want to create a webloc:
cat << 'EOF' > ~/Downloads/OpenWebsite2.webloc
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>URL</key>
<string>https://nei.lt</string>
</dict>
</plist>
EOF
This is fine but has a number of issues:
- the location and filename of the output file are hard coded
- you have to go through and change the url in the string every time
The answer to this then is to create a shell script:
#!/bin/zsh
if [ "$#" -lt 2 ]; then
echo "Usage: makewebloc <URL> <OUTPUT_PATH>"
echo "Example: makewebloc \"https://example.com\" ~/Desktop/MyLink.webloc"
exit 1
fi
URL="$1"
OUTFILE="$2"
# Ensure the output filename ends with .webloc
if [[ "$OUTFILE" != *.webloc ]]; then
OUTFILE="${OUTFILE}.webloc"
fi
cat << EOF > ${OUTFILE}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>URL</key>
<string>$URL</string>
</dict>
</plist>
EOF
Bingo! Job Done. Not quite.
Solution #2
When I tried running the script with this URL: https://nei.lt?this=does¬=work it failed with the following error:
The document content is not readable or is in the wrong format.
Turns out the script, or more accurately the XML, doesn’t like the ampersands so we need to change them from & to the more safe equivalent of %26.
#!/bin/zsh
if [ "$#" -lt 2 ]; then
echo "Usage: makewebloc <URL> <OUTPUT_PATH>"
echo "Example: makewebloc \"https://example.com\" ~/Desktop/MyLink.webloc"
exit 1
fi
URL="$1"
SAFEURL="${URL//&/%26}"
SAFEURL="${SAFEURL//</%3C}"
SAFEURL="${SAFEURL//>/%3E}"
OUTFILE="$2"
# Ensure the output filename ends with .webloc
if [[ "$OUTFILE" != *.webloc ]]; then
OUTFILE="${OUTFILE}.webloc"
fi
cat << EOF > ${OUTFILE}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>URL</key>
<string>$SAFEURL</string>
</dict>
</plist>
EOF
Now this does work but what if you wanted to omit the output file and have the file named from the title of the page?
Solution #3
At this point I must admit that a bash script to do this was beyond my skills and, it turned out, was too much for Ai to manage as well as it had to drop into Python to get it done.
This script uses Python to extract the title property from the HTML handling the user agent, certificates and some edge cases with Google Docs which is what I originally wanted the script for. It also creates the output file as a binary plist file which isn’t really necessary. To be honest this is all a bit of overkill and so I use option 2. YMMV as they say.
#!/bin/zsh
unsetopt nomatch
if [ "$#" -lt 1 ]; then
echo "Usage: makewebloc <URL> [OUTPUT_DIR]"
echo "Example: makewebloc \"https://example.com\" ~/Desktop"
exit 1
fi
URL="$1"
OUTDIR="${2:-.}"
python3 -c '
import sys, os, re, plistlib, ssl
from urllib.request import Request, urlopen
from html.parser import HTMLParser
url = sys.argv[1]
outdir = os.path.expanduser(sys.argv[2])
# Adjust Google Drive/Docs URL to mobile basic view if applicable to bypass 401 prompt
fetch_url = url
if "docs.google.com" in url or "drive.google.com" in url:
fetch_url = re.sub(r"/edit.*", "/mobilebasic", url)
class TitleParser(HTMLParser):
def __init__(self):
super().__init__()
self.in_title = False
self.title = ""
self.og_title = ""
def handle_starttag(self, tag, attrs):
if tag.lower() == "title":
self.in_title = True
elif tag.lower() == "meta":
attr_dict = {k.lower(): v for k, v in attrs if k and v}
if attr_dict.get("property") == "og:title" or attr_dict.get("name") == "title":
self.og_title = attr_dict.get("content", "")
def handle_endtag(self, tag):
if tag.lower() == "title":
self.in_title = False
def handle_data(self, data):
if self.in_title:
self.title += data
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
page_title = ""
try:
# Use standard search crawler User-Agent to retrieve og:title metadata without auth challenges
headers = {"User-Agent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"}
req = Request(fetch_url, headers=headers)
with urlopen(req, context=ssl_context, timeout=5) as response:
html = response.read().decode("utf-8", errors="ignore")
parser = TitleParser()
parser.feed(html)
page_title = parser.og_title or parser.title or "Bookmark"
# Clean up Google branding suffixes
page_title = re.sub(r"\s*-\s*Google (Docs|Drive|Sheets|Slides)$", "", page_title)
page_title = re.sub(r"^Google Docs\s*-\s*", "", page_title)
except Exception as e:
print(f"Warning: Could not fetch page title ({e}). Using default name.")
page_title = "Bookmark"
# Sanitize title for macOS filenames
safe_filename = re.sub(r"[\/\\:\*\?\"<>\|]", "_", page_title)
safe_filename = re.sub(r"\s+", " ", safe_filename).strip()
filename = f"{safe_filename}.webloc"
filepath = os.path.join(outdir, filename)
# Write binary plist
data = {"URL": url}
with open(filepath, "wb") as f:
plistlib.dump(data, f, fmt=plistlib.FMT_BINARY)
print(f"Saved: {filepath}")
' "$URL" "$OUTDIR"