Creating a website link file in macOS

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?

Yes, you can do it like this!

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:

  1. the location and filename of the output file are hard coded
  2. 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&not=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"

Leave a Reply

Your email address will not be published. Required fields are marked *