I published my first post on the newly migrated Feld Thoughts this morning - a writeup of moving 5,530 WordPress posts to Hugo . Then I changed the title. The original slug said “migrating-feld.com” but it should have said “migrating-feld-thoughts.” I pushed the rename and moved on.

A few minutes later, William Mougayar clicked the link in the first email, got a 404, and sent me a note. Then, I got one from Rick Levine with a “chuckle.” That’s how I found out Kit - the email service that watches my RSS feed - had already sent the first version to subscribers. And now it was sending the second version too. Every subscriber got two emails for the same post, and the link in the first one was dead.


The immediate fix was simple. I added a Vercel 301 redirect from the old URL to the new one in vercel.json so anyone clicking the link in the first email wouldn’t get a 404. That took about two minutes.

The more interesting question was why Kit sent two emails in the first place. I dug into Hugo’s RSS template and found the answer on line 74:

<guid>{{ .Permalink }}</guid>

Hugo uses the post’s permalink as the RSS <guid> element. The RSS spec defines <guid> as the unique identifier for an item - it’s how every RSS reader and email automation decides whether a post is new. When I renamed the slug, the permalink changed, the GUID changed, and Kit saw what it interpreted as a brand new post. It had no way to know this was the same post with a different URL. RSS has no concept of “this replaces that.”

This is the same reason WordPress generates opaque GUIDs like ?p=12345 that never change regardless of how many times you edit the title or slug. It seems like an arbitrary design choice until you hit exactly this problem.


I fixed this at three layers. First, I modified Hugo’s RSS template to check for a custom guid field in front matter before falling back to the permalink. Second, I added an explicit slug field to the post’s front matter - this decouples the URL from the directory name, so renaming the directory doesn’t change the URL. Third, I added Hugo aliases for the old URL path, which generates an HTML redirect page at build time as a belt-and-suspenders backup to the Vercel redirect.

The template change is one line:

<guid>{{ with .Params.guid }}{{ . }}{{ else }}{{ .Permalink }}{{ end }}</guid>

Old posts without a guid in front matter still use the permalink - fully backward compatible. New posts get a frozen GUID set at publish time that never changes.

I updated both the Feld Thoughts and Adventures in Claude publish workflows. The /blog-feld and /blogaic-post commands now set slug and guid at publish time, and if you rename a published post, they auto-detect the change and add an alias redirect for the old URL.


The migration scripts for moving from WordPress to Hugo are open-source at github.com/bradfeld/wp-to-hugo . Five scripts that handle export, custom post types, media download with reference counting, entity cleanup, and sitemap verification. The full writeup covers the whole process.

The RSS GUID problem is the kind of thing you only discover by doing. WordPress solved it years ago with opaque IDs. Hugo’s default of permalink-as-GUID works fine until you rename something - and then every subscriber gets a duplicate email with no way to undo it. Now both sites have the fix baked in at the template level, and the publish commands enforce it going forward.