This reference explains how Discourse permalinks redirect old URL paths to new destinations, how permalink normalizations work, and how to test and troubleshoot redirects after a migration or URL structure change.
Required user level: Administrator
Console access required only when creating permalinks from an importer, script, or Rails console
When you migrate from another forum or change your site’s URL structure, old links from search engines, bookmarks, emails, or other websites may no longer point to the correct page.
Permalinks let you redirect old URL paths to their new destinations using 301 Moved Permanently redirects.
Permalinks are exact path mappings. For example, if this old URL no longer works:
https://discourse.example.com/forum/topic/123
you can create a permalink so it redirects to the new topic:
https://discourse.example.com/t/welcome-to-our-community/456
What permalinks are
A permalink is an exact mapping from one old URL path to one new destination.
For example:
forum/topic/123 → /t/welcome-to-our-community/456
Permalinks are useful when:
- you migrated from another forum platform
- old links are indexed by search engines
- old links are used in emails, documentation, or other websites
- you changed the structure of your site’s URLs
- you want an old path to redirect to a topic, post, category, tag, user, or external URL
Each permalink URL maps to one destination.
Permalinks are exact matches. The permalink URL field does not support wildcards or regular expressions. If many old URLs follow a predictable pattern, use permalink normalizations to transform incoming URLs before the exact permalink lookup.
Permalinks and normalizations
Permalinks and normalizations do different things:
| Feature | What it does | Example |
|---|---|---|
| Permalink | Redirects one saved old path to one destination | old/topic/123 → Topic 456 |
| Normalization | Changes the incoming path before permalink lookup | old/topic/123-title → old/topic/123 |
A normalization does not redirect by itself. It only changes the path that is used to look up a saved permalink.
You still need a matching permalink after the normalization is applied.
Quick examples
Redirect an old topic URL
Old URL:
https://discourse.example.com/forum/topic/123
Permalink URL:
forum/topic/123
Destination type:
Topic
Destination:
456
Result:
/forum/topic/123 → /t/welcome-to-our-community/456
Redirect an old URL with a query string
Old URL:
https://discourse.example.com/viewtopic.php?t=123
Permalink URL:
viewtopic.php?t=123
Destination type:
Topic
Destination:
456
Result:
/viewtopic.php?t=123 → /t/welcome-to-our-community/456
Redirect many old URLs with the same pattern
Old URLs:
/forum/support/123-how-do-i-reset-my-password.html
/forum/general/456-how-do-i-change-my-email.html
You can use a normalization to simplify them before lookup:
/forum/support/123-how-do-i-reset-my-password.html → forum/123
/forum/general/456-how-do-i-change-my-email.html → forum/456
Then create exact permalinks:
forum/123 → Topic 1001
forum/456 → Topic 1002
What to enter as the permalink URL
Enter the old path, not the full URL.
For example, use:
forum/topic/123
or:
/forum/topic/123
Do not use the full old URL:
https://discourse.example.com/forum/topic/123
Leading slashes are accepted, but they are removed when the permalink is saved. These two entries are equivalent:
/forum/topic/123
forum/topic/123
Both are stored as:
forum/topic/123
Leading and trailing whitespace is stripped when the permalink is saved.
Query strings are part of the match
Permalinks match the full request path, including the query string.
That means these two URLs are different permalink matches:
/old/topic/123
/old/topic/123?utm_source=example
If the old URL has a query string, either:
- create a permalink that includes the query string, or
- use a permalink normalization to remove or simplify the query string before matching
For most analytics or tracking parameters, using a normalization is usually better than creating many separate permalinks.
For example, this saved permalink:
docs/123
does not necessarily match this requested URL:
/docs/123?utm_source=newsletter
unless a normalization removes the query string before lookup.
When copying old URLs from analytics tools, search console reports, browsers, or email campaigns, check whether tracking parameters have been added.
URL fragments are not part of the match
URL fragments are not sent to the server.
For example, this URL:
/old/topic#post-12
arrives at the server as:
/old/topic
A permalink or normalization cannot match:
#post-12
unless the # is encoded in the actual request as %23.
Supported permalink destinations
A permalink can redirect to one of these destination types:
- Topic
- Post
- Category
- Tag
- User
- External or relative URL
External or relative URL destinations can be used for redirects such as:
old/privacy-policy → https://archive.discourse.example.com/privacy
or:
old/preferences → /my/preferences
Use external URL destinations carefully. They redirect visitors away from your site and do not check whether the target URL exists.
Prefer internal destination types when redirecting to migrated topics, posts, categories, tags, or users. Use an external URL destination when the target page is not represented by an internal object.
Add a permalink manually
To add a permalink manually:
- Go to
/admin/config/permalinks. - Select the Permalinks tab.
- Click Add permalink.
- Enter the old path in the URL field.
- Select the Permalink type.
- Enter or select the destination.
- Save the permalink.
- Test the old URL in a browser or with
curl.
Example:
URL:
forum/topic/123
Permalink type:
Topic
Destination:
456
A request to:
https://discourse.example.com/forum/topic/123
will redirect to topic 456.
Destination values
The destination value depends on the permalink type.
| Permalink type | Destination |
|---|---|
| Topic | A topic |
| Post | A post |
| Category | A category |
| Tag | A tag |
| User | A user |
| External or relative URL | A full external URL or a relative path |
Examples:
forum/topic/123 → Topic 456
forum/post/789 → Post 789
forum/category/support → Category support
forum/tag/example → Tag example
forum/user/alice → User alice
old/privacy → https://archive.discourse.example.com/privacy
old/preferences → /my/preferences
Creating permalinks during a migration
Many official importers create permalinks automatically for migrated categories, topics, posts, or users.
For large migrations, permalinks are usually created by the importer or by a migration script rather than entered manually one at a time.
If you are writing a custom importer, create permalink records for old URLs that should continue to work after the migration.
For example:
Permalink.create!(url: "discussion/12345", topic_id: 987)
In this example, discussion/12345 is the old path, and 987 is the Discourse topic ID.
A leading slash is also accepted:
Permalink.create!(url: "/discussion/12345", topic_id: 987)
Both examples store the permalink URL as:
discussion/12345
To redirect to an external URL:
Permalink.create!(
url: "discussion/12345",
external_url: "https://archive.discourse.example.com/discussion/12345"
)
When creating permalinks in an importer:
- store the old path, not the full old domain
- include the query string only if it is required for matching
- make sure each old path maps to one destination
- check for duplicates before inserting records
- remember that permalink URLs must be unique
If you run multiple test migrations, final topic and post IDs may change between runs. Generate or verify your final permalink mappings when the production migration data is stable.
What permalink normalizations are
A permalink normalization changes an incoming old path before looking for a matching permalink.
Normalizations are useful when many old URLs follow the same predictable pattern.
For example, suppose your old forum used URLs like this:
/forum/support/123-how-do-i-reset-my-password.html
but your saved permalink is simpler:
forum/123
A normalization can transform the incoming request into the saved permalink path:
/forum/support/123-how-do-i-reset-my-password.html
↓
forum/123
Then the permalink can redirect:
forum/123 → /t/how-do-i-reset-my-password/456
A normalization does not redirect by itself. It only changes the path used for the permalink lookup.
You still need a matching permalink for the normalized path.
Most sites do not need permalink normalizations. Use them only when many old URLs follow a predictable pattern and creating one permalink per old URL would be impractical.
How permalinks and normalizations work together
When an old URL is requested, the process is:
Visitor requests old URL
↓
Permalink normalizations are applied, if configured
↓
A permalink lookup is performed using the resulting path
↓
If a matching permalink exists, the visitor is redirected
For example:
Requested URL:
/old/topic/123?utm_source=newsletter
Normalization changes it to:
old/topic/123
Permalink redirects it to:
/t/new-topic-title/456
Add a permalink normalization
Permalink normalizations are configured from the permalink settings.
To add one:
- Go to
/admin/config/permalinks. - Select the Settings tab.
- Add a rule to the
permalink normalizationssetting. - Save the setting.
- Test an old URL that should be normalized.
- Confirm that the normalized path matches an existing permalink.
The syntax is:
/<regex>/<replacement>
Multiple rules are separated with |.
For example:
/old\/topic\/(\d+).*/topic/\1
This can transform:
old/topic/123-my-old-title
into:
topic/123
The replacement uses Ruby replacement syntax, so capture groups are written as:
\1
\2
\3
Important normalization rules
Keep normalization rules simple.
- Normalizations use Ruby regular expressions.
- Literal
/characters in the regex part must be escaped as\/. - Literal
/characters in the replacement part do not need to be escaped. - Multiple normalization rules are separated with
|. - Because
|separates rules, avoid using regex alternation with|. - Each rule uses a single replacement, not a global replacement.
- Rules are applied in order.
- Matching rules are applied sequentially.
- Normalizations are applied before permalink lookup.
- Normalizations are also applied when saving permalink records.
- The setting validator catches invalid regular expressions, but it may not catch every rule that behaves differently than intended.
Warning: Normalizations are applied when permalink records are saved. If you enter a non-normalized URL into the admin UI, it may be stored as the normalized result. If you add or change normalizations after creating permalinks, test carefully and check how newly saved permalink URLs are stored.
Normalization examples
Redirect an old URL with extra title text
Old URLs:
/forum/support/123-how-do-i-reset-my-password.html
/forum/general/456-how-do-i-change-my-email.html
You can normalize them to simpler permalink paths:
forum/123
forum/456
Example normalization:
/forum\/[^\/]+\/(\d+).*/forum/\1
Then create permalinks:
forum/123 → Topic 1001
forum/456 → Topic 1002
Ignore tracking query strings
Old requested URL:
/docs/123?utm_source=newsletter
Saved permalink:
docs/123
Normalization:
/(docs\/\d+)\?.*/\1
This removes the query string before permalink lookup.
Preserve an important query parameter
Some old forums use query parameters as the stable topic identifier.
For example:
viewtopic.php?f=10&t=123
viewtopic.php?t=123
You may want to normalize these to:
viewtopic.php?t=123
Example normalization:
/(viewtopic\.php\?)(?:.*&)?(t=\d+).*/\1\2
Then create a permalink:
viewtopic.php?t=123 → Topic 456
Regex and wildcard redirects
The permalink URL field does not support regexes or wildcards.
This will not work as a permalink URL:
forum/topic/*
This will not work as a permalink URL either:
forum/topic/(\d+)
If you need pattern-based handling, use a permalink normalization to rewrite the incoming URL to a form that can match an exact saved permalink.
For example:
/forum/topic/123-title → forum/topic/123
Then create a normal exact permalink:
forum/topic/123 → Topic 456
Route precedence and built-in paths
Permalink routing is checked after normal application routes.
This means a valid existing route may resolve normally before a permalink is checked.
Be careful with old URLs that begin with built-in paths such as:
/t
/c
/u
/tag
/tags
For example, an old forum URL like this may look like a built-in category route:
/c/blog/old-platform-url/ba-p/12345
If an old URL conflicts with a valid existing route, the valid route can take precedence over the permalink.
Similarly, if you create a permalink for a path that looks like an existing topic route, such as:
t/123
the normal topic route may be handled before the permalink lookup.
Some built-in not-found routes have additional fallback behavior, but you should not rely on that. Always test old URLs that overlap with built-in paths.
If you need an old topic URL to redirect somewhere else, the old topic generally must not continue to resolve as a valid topic route.
Permissions and private content
Permalinks to internal destinations respect normal permissions.
If a permalink points to a private or restricted topic, post, category, tag, or user, visitors who cannot access that destination will get a 404 instead of a redirect.
Permalinks to external URLs do not check internal content permissions.
Special characters and encoded URLs
Test URLs with special characters carefully.
This includes URLs with:
- spaces
+%&':- parentheses
- non-Latin characters
Permalink matching uses the encoded request path after removing the leading slash and applying configured normalizations. Encoding differences can prevent a URL from matching the permalink you expect.
For example, these may not be equivalent depending on how the old URL is requested and stored:
old/topic/hello%20world
old/topic/hello+world
Use %20 for spaces in URL examples. In a URL path, + is not the same as a space.
Characters such as &, ?, and # have special meaning in URLs. If they are intended to be literal path characters, they should be percent-encoded.
When in doubt, test the exact old URL that users or search engines will request.
Limitations
Permalinks are designed for redirecting old paths to new destinations. They are not a general-purpose redirect or rewrite engine.
Important limitations:
- Permalink URLs are stored as path and query strings, not full URLs.
- Full URLs are not automatically converted to paths.
- Permalink URLs must be unique after normalization.
- A permalink URL can have only one destination.
- Supported destinations are limited to topics, posts, categories, tags, users, and external or relative URLs.
- Permalinks use
301 Moved Permanently; there is no per-permalink option for302. - The permalink URL field does not support wildcards or regular expressions.
- Query strings are part of the lookup key.
- URL fragments, such as
#post-12, are not sent to the server and cannot be matched. - Matching does not use the request scheme or host.
- Native routes are checked before the catch-all permalink route.
- Internal destinations are permission checked.
- External URL destinations bypass internal content permission checks.
- Permalink normalizations use Ruby regular expressions.
- Normalization rules are separated with
|, so avoid using|as regex alternation. - Normalization rules use a single replacement per rule, not a global replacement.
- Normalizations are applied both before lookup and before saving permalink records.
- The setting validator catches invalid regular expressions, but it may not catch every rule that behaves differently than intended.
For most sites, simple one-to-one permalinks are easier to maintain than complex normalization rules. Use normalizations only when old URLs follow a predictable pattern.
Avoid redirecting every old URL to the homepage
Do not redirect all old URLs to your homepage.
Redirect each old URL to the most relevant new page. If there is no equivalent content, a 404 may be better than sending users and search engines to an unrelated page.
Good redirects are specific:
old/topic/123 → new topic about the same subject
Poor redirects are generic:
old/topic/123 → homepage
old/topic/456 → homepage
old/topic/789 → homepage
Test your redirects
After creating permalinks or normalizations, test a sample of old URLs.
You can test in a browser, or use:
curl -I https://discourse.example.com/forum/topic/123
A working permalink redirect should return a permanent redirect similar to:
HTTP/2 301
location: https://discourse.example.com/t/welcome-to-our-community/456
Test examples from each old URL pattern, including:
- old topic URLs
- old category URLs
- old post URLs
- URLs with query strings
- URLs with special characters
- URLs from search results
- URLs from old emails or documentation
- URLs that start with built-in paths like
/t,/c, or/u - external URL redirects
Troubleshooting
If an old URL does not redirect, check these common causes:
- Confirm the permalink exists.
- Confirm the permalink URL is the old path, not the full old domain.
- Check whether the old URL includes a query string.
- If using a normalization, confirm the normalized path matches a saved permalink.
- Check whether a built-in route is taking precedence.
- Check whether the destination is private or restricted.
- Check for encoding differences, such as
%20vs+. - Check whether a normalization changed the URL when the permalink was saved.
- Confirm the content was migrated.
- Test with the exact old URL from a browser or with
curl -I.
The most common issues are:
- entering the full old URL instead of the old path
- missing or unexpected query strings
- expecting wildcards or regexes to work in the permalink URL field
- old URLs overlapping with built-in paths
- private or restricted destinations
- encoding differences
- normalizations changing the path unexpectedly
SEO notes
After a migration, search engines may report old URLs as redirected. This is expected if those old URLs now redirect to the correct new pages.
For best results:
- redirect old URLs to relevant new pages
- avoid unnecessary redirect chains
- avoid redirecting many unrelated URLs to the homepage
- keep redirects in place long enough for users and search engines to update
- test important URLs from your old sitemap, analytics, or search console data
Last edited by @ruben 2026-06-17T23:25:56Z
Last checked by @ruben 2026-06-17T23:25:29Z
Check document
Perform check on document: