# WordPress permalinks: from ?p=123 to clean URLs, rankings intact

> Move a WordPress site from ?p=123 addresses to readable URLs, choose the right structure, set up the server rewrite rules and redirects, and test that every old link still lands on the right page.

Updated 2026-09-25 · WordPress & WooCommerce · HTML version: https://getreport.app/guides/wordpress-permalinks

A permalink is the permanent address of a post or page. WordPress can build it in six ways, and the one a fresh install falls back to when the server cannot rewrite URLs, `/?p=123`, is the worst of them: no words, nothing to trust in a search result, and an address that means nothing outside this one database. Switching is one click. Switching without losing rankings takes a plan, which this guide gives you. Allow 15 minutes on a new site and an evening on an established one.

## Quick answer

- Choose **Post name** (`/sample-post/`) in Settings → Permalinks unless you have a strong reason not to.
- From **Plain** to anything else: WordPress redirects the old `?p=` addresses itself.
- From one readable structure to another (say, dates to post name): add **301 redirects** from the old pattern, because WordPress only guesses.
- The server must rewrite URLs: `mod_rewrite` and the WordPress block in `.htaccess` on Apache, `try_files` on nginx.
- Pick one trailing-slash style and keep links, sitemap and redirects consistent with it.
- Test 20 old URLs after the change; each should reach the new address in one hop.

## Why permalinks matter

People decide whether to click a search result partly on the address under the title. `example.com/leather-bag-care/` tells them what they will get; `example.com/?p=4182` does not. The same applies when a link is pasted in a chat or email, where the address is often all there is.

For search engines, words in the URL are a small signal, but a stable, readable URL structure matters more than it looks: it makes internal links readable in the editor, analytics reports readable by humans, and a future migration to another CMS possible without mapping thousands of numeric IDs by hand. The [URL structure](https://getreport.app/learn/url-structure) learn page covers the general rules.

The risk runs the other way too. Every address the site has ever published has links pointing at it, from your own pages, other sites and Google's index. Change the structure without redirects, and every one of them turns into a 404 at once. That, not the choice of structure, is what loses rankings. With redirects in place, expect some movement in search for a few weeks while Google recrawls the old addresses and moves their signals to the new ones; without them, the losses are permanent for every page that had links.

## How getReport checks it

> **Free tool:** [WordPress health check](https://getreport.app/tools/wordpress-checker): Is your WordPress site indexed, up to date and free of the classic launch mistakes? Checks "Discourage search engines", default content and tagline, plain permalinks, attachment pages, the exposed admin user, a public staging copy and the core version, plus security basics.

The [WordPress health check](https://getreport.app/tools/wordpress-checker) reads the links on the page it fetched. When two or more same-host links use `?p=`, `?page_id=`, `?cat=` or `?attachment_id=`, it raises the plain-permalinks finding and lists the examples, so you can see where they come from (menus, widgets, the content itself).

![The plain permalinks finding card on a WordPress site whose menu links end in /?p= and /?page_id=, with the fix pointing to Settings → Permalinks and the evidence line listing the links it found](https://getreport.app/guides/img/wordpress-permalinks/finding.webp "The finding lists the ?p= links it found on the page, which is also the list to update after the switch.")

> **Check: Permalinks use readable slugs.** Addresses like ?p=123 carry no keywords, look untrustworthy in search results and break when content is migrated. Every other permalink setting is better.
>
> 1. Go to Settings → Permalinks and choose "Post name". WordPress redirects the old ?p= addresses automatically.
> 2. Update any hard-coded links in menus and widgets afterwards.

Three general URL findings from the same report matter when you change the structure:

> **Check: URL length.** Long URLs are cut off in search results and hard to share by hand. Short, readable URLs get more clicks and fewer typos.
>
> 1. Shorten the slug to 3–5 meaningful words and drop tracking or session parameters from the canonical URL.

> **Check: Internal links use trailing slashes consistently.** /shop/ and /shop are two different URLs to a search engine. Linking to both styles creates duplicate pages and splits their signals.
>
> 1. Choose one style (with or without trailing slash) and redirect the other to it with a 301.
> 2. Update internal links and the sitemap to use the chosen style everywhere.

> **Check: The URL loads without a redirect chain.** Each redirect is a full round trip before the browser can start loading, often 100–300 ms on mobile. Search engines pass less value with every hop and stop following after a few.
>
> 1. Point every old URL straight at the final one (a single 301), and update internal links to use the final URL directly.
> 2. Look for rules that stack, for example http → https, then non-www → www, then trailing slash; combine them into one rule.

## The six structures

Settings → Permalinks offers:

| Setting | Example | Verdict |
| --- | --- | --- |
| Plain | `/?p=123` | Avoid |
| Day and name | `/2026/09/25/sample-post/` | News sites only |
| Month and name | `/2026/09/sample-post/` | News sites only |
| Numeric | `/archives/123` | Avoid |
| Post name | `/sample-post/` | Best default |
| Custom Structure | `/%category%/%postname%/` | Only with a stable category tree |

**Post name** is right for most sites: short, readable, and it does not change when you reorganise the site. Pages always use their own slug and parent pages (`/services/design/`) whatever you choose; the setting applies to posts.

**Dates** make sense where the date is part of the content: a news site, a changelog. On evergreen content they make a 2021 article look stale in the result, even after you update it.

**Category and post name** (`/%category%/%postname%/`) reads well and groups content in analytics, but a post's URL now depends on its category. Rename or merge a category and every post in it moves. Choose it only if the category tree is fixed.

**Custom Structure** accepts the tags `%year%`, `%monthnum%`, `%day%`, `%hour%`, `%minute%`, `%second%`, `%post_id%`, `%postname%`, `%category%` and `%author%`. Two rules keep a custom structure safe: end it with `%postname%` or `%post_id%` so every post has a unique address, and leave out `%author%` unless authors never change, because reassigning a post moves it.

The **Optional** section below sets the category and tag bases (`/category/`, `/tag/` by default). Leave them unless you have a reason; Yoast and Rank Math can remove the `/category/` prefix entirely, and that change needs the same redirect care as the structure itself.

## Step by step

### 1. Back up and pick a quiet hour

Take a full backup (files and database). Change the setting when traffic is lowest; a mistake in the rewrite rules makes every page except the home page return 404 until it is fixed.

### 2. Export a list of current URLs

Before anything changes, save the current addresses: download the XML sitemap (`/wp-sitemap.xml` or `/sitemap_index.xml` from an SEO plugin), or export the top landing pages from Search Console → Performance → Pages. This is your test list and, for a structure-to-structure change, your redirect map.

### 3. Check that the server can rewrite URLs

On **Apache**, WordPress writes this block to `.htaccess` in the site root when you save the permalink settings. It needs `mod_rewrite` enabled and `AllowOverride` permitting it. If the file is not writable, WordPress shows the block for you to paste:

```apache
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
```

On **nginx**, `.htaccess` is ignored. The `server` block needs:

```nginx
location / {
    try_files $uri $uri/ /index.php?$args;
}
```

Without it, pretty URLs return nginx's own 404 page. **Caddy**'s `php_fastcgi` directive already includes the equivalent `try_files`.

### 4. Switch the setting

Settings → Permalinks → Post name → Save Changes. Open a post from the front end to confirm it loads at its new address.

### 5. Redirect the old addresses

**From Plain:** nothing to add. A request for `/?p=123` or `/?page_id=45` is answered with a 301 to the new address by WordPress's canonical redirect.

**From one readable structure to another:** WordPress tries to guess the right post when an old address 404s, matching by slug, but it is a guess and fails on renamed or duplicated slugs. Add an explicit rule. Dates to post name on Apache, above the `# BEGIN WordPress` block:

```apache
RedirectMatch 301 ^/\d{4}/\d{2}/\d{2}/(.+)$ /$1
```

On nginx, inside the `server` block before the `location /` block:

```nginx
# Quoted because the pattern contains curly braces
rewrite "^/\d{4}/\d{2}/\d{2}/(.+)$" /$1 permanent;
```

For "Month and name", drop one `/\d{2}` from the pattern. Without server access, the **Redirection** plugin (Tools → Redirection) accepts the same pattern as a regex redirect: source `^/\d{4}/\d{2}/\d{2}/(.+)$`, target `/$1`, 301. Its "Monitor changes" option also adds a redirect whenever you later change a post's slug. WordPress core already does that for posts, but not for pages.

### 6. Update links that bypass permalinks

Menus built with the menu editor update themselves. Links typed by hand into content, widgets, the footer or the theme options keep the old address and now go through a redirect. The Better Search Replace plugin updates them in one pass (back up first). The steps in [finding and fixing broken links](https://getreport.app/guides/find-and-fix-broken-links) catch the rest, page by page.

### 7. Test 20 old URLs

Take 20 addresses from the list in step 2, weighted towards pages with traffic and backlinks, and run each through the [redirect checker](https://getreport.app/tools/redirect-checker). Every one should answer one 301 to the new address, then 200. If you see two hops (old → http → https, or old → no slash → slash), fold them into one; [redirects without chains](https://getreport.app/guides/redirects-without-chains) shows how.

## Platform notes

### WooCommerce

Product URLs have their own settings on the same screen under **Product permalinks** (`/product/`, `/shop/` or a custom base). The same rules apply: change once, redirect the old pattern.

### Multilingual sites

WPML and Polylang add a language prefix or directory. Change the structure on staging first and test one URL per language.

### Trailing slashes

WordPress follows the structure you saved: `/%postname%/` gives addresses with a trailing slash and redirects the variant without one. Keep hand-written links, canonical tags and the sitemap on that same style, so no internal link costs a redirect.

## Verify

- The WordPress health check shows the plain-permalinks finding as passed, and the trailing-slash finding reports consistent links.
- All 20 test URLs reach their new address with a single 301.
- Search Console → Pages shows no jump in "Not found (404)" over the next two weeks, and the new URLs start replacing the old ones in the Performance report.

## Common mistakes

- **Changing structure on a large site without a redirect map.** Thousands of 404s at once, and Google drops the pages it cannot find. Build the redirect rule first, test it on staging, then switch.
- **Dates in URLs for evergreen content.** Articles look old in search results and every refresh raises the question of a new URL. Use Post name.
- **Obsessing over stop words.** Removing "the" and "and" from slugs changes nothing measurable, and changing the slug of a page that already ranks needs a redirect. Keep slugs short, readable and stable.
- **Switching on nginx without `try_files`.** Every post returns 404 while the home page works. Add the `location /` block and reload nginx.
- **Removing the `/category/` base as an afterthought.** Every category archive moves, and the old addresses 404. Treat it like a structure change: redirect `/category/(.+)` to `/$1` and test.
- **Stacked redirects.** An old rule from the http-to-https move plus a new one from the permalink change means two hops. Point old addresses straight at the final URL.
