guest@gitkeniwo.github.io: ~
~> glow -p 2025-11-12-migrate-blog-images.md

Migrate Blog Images to New Domain Name

Image hosting can be a pain in your arse, especially when you have a legacy blog with legacy posts from when you were young and didn’t know stuff about maintenance and cloud storage, CLI tools, all that jazz. Thankfully now i’m a bit more experienced than before. With the new domain name i got for free in my Github student pack, let’s destroy all these hideous image links once and for all.

Let’s begin by Ripgreping all the image links and output to a text file:

rg -io -N -I \
  -g '!2025-11-12-migrate-blog-images.md' \
  -g '!2020-11-24-blog-migration.md' \
  -e '(https?://[^") >]+?\.(jpe?g|png|gif|webp))' \
  -e '((\.?/|/)[^") >]+\.(jpe?g|png|gif|webp))' \
  source/_posts/ | sort -u > all_image_links.txt

This will give you something like:

───────┬───────────────────────────────────────────────────────────────────────────────────
       │ File: all_image_links.txt
───────┼───────────────────────────────────────────────────────────────────────────────────
   1   │ ./img/b674-iufmpmp1256471.png
   2   │ /440px-Charles_Gleyre_-_Daphnis_et_Chloe_revenant_de_la_montagne.jpg
   3   │ /img/20-06-06-darkmode.jpg
   4   │ /img/20-06-06-pic.png
   5   │ /img/20-06-06-tag.png
   6   │ /img/20-06-06-tag1.png
   7   │ /img/20-06-06-tag2.png
   8   │ /img/20-06-06-tag3.png
   9   │ /img/20-06-09-pretapork.jpg
  10   │ /img/20-06-09-siri.jpg
  11   │ /img/20-06-someWalrus_simplified_bit_notation.png
  12   │ /img/20-11-10.png

You see. I was a messy man before, with all sort of image links here and there. Some were uploaded to Imgur, some are shamelessly hotlinked from a dedicated Github repo, or exploiting imgur, some are just primitive relative paths to my local source/img/ folder, and some are already in my R2 bucket and need migration to the new domian name.

After manually picking out some apparently wrong links, now we can script for each of these types of links, migrating them accordingly. For local images and images in other places, we would like to upload them again to our R2 bucket first. I have piclist-core cli configured on my mac, so this will be our go-to connector to R2.

Since the http API of piclist is a lot of work to deal with, let’s just use the CLI, and generate the commmands using python scripts in a jupyter notebook.

# classify_images.py
from pathlib import Path

urls = Path("all_image_links.txt").read_text().splitlines()

def classify(url):
    if "img.gitkeniwo.me" in url:
        return "r2_existing"
    if any(x in url for x in [
        "githubusercontent", "github.com", "imgur.com",
    ]):
        return "needs_upload"
    if url.startswith(("/")):
        return "local"
    return "unknown"

groups = {"r2_existing": [], "needs_upload": [], "local": [], "unknown": []}
for url in urls:
    groups[classify(url)].append(url)

for name, items in groups.items():
    Path(f"images_{name}.txt").write_text("\n".join(items))
    print(f"{name}: {len(items)} entries")

This helps you classify the images into 4 groups: existing R2 images, images that need to be re-uploaded, local images and irrelevant/unknown images. Now deal with each group one by one.

For local images, this gives you the picgo cli command to upload them all:

with open("images_local.txt", "r") as f:
    local_images = f.read().splitlines()
    
print("picgo u " + " ".join(["source" + i for i in local_images])) 

Print it out, copypaste and run it. You will get a list of new image links. Copypaste them back to the ipynb and parse them into a list replaced_local_links.

Remember when configuring your picgo R2 uploader, add a md5 hash to your filenaming pattern, because simple filename + timestamp could still cause duplication when you feed same-named files simultaneously.

Now we have the old links and the new links. Create a sed command generator:

def create_sed_commands(images, replaced_links):
    sed_commands = []
    for original, replaced in zip(images, replaced_links):
        command = f"rg -l '{original}' source/_posts/ |" \
            + f" xargs sed -i '' 's#{original}#{replaced}#g'"
            
        sed_commands.append(command)
        
    # save to a .sh file
    with open("replace_image_links.sh", "w") as f:
        f.write("\n".join(sed_commands))

And just run the .sh script in your hexo root folder.

$ chmod +x replace_image_links.sh
$./replace_image_links.sh

Done replacing the old image links.

The same applies to Github images that need to be re-uploaded. Just generate the picgo upload commands, run them, get the new links, generate and execute the sed commands to replace them.

For existing R2 images, we don’t need to re-upload. Just use urllib to replace the netloc part of the url from img.gitkeniwo.me to img.gitkeniwo.tech.

from urllib import parse

new_images = []
for url in old_images:
    parsed = parse.urlparse(url)
    new_netloc = parsed.netloc.replace("img.gitkeniwo.me", "img.gitkeniwo.tech")
    new_url = parsed._replace(netloc=new_netloc).geturl()
    new_images.append(new_url)

And generate corresponding sed commands as before.

Of course, if it’s a imgur link, you could get 502 when uploading with picgo cli. This needs to be manually done and i am lucky enough to have only a handful of imgur links.

(END)
~>