Skip to content
Merged
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
72b68e3
wip
duncanmcclean Apr 20, 2026
e6ac2f1
add url validation
duncanmcclean Apr 20, 2026
05567ac
rename to response code
duncanmcclean Apr 20, 2026
251a5b7
instructions
duncanmcclean Apr 20, 2026
ebd7807
Extract redirect logic into dedicated class and add config to preserv…
duncanmcclean Apr 20, 2026
2a521d5
Merge branch '7.x' into redirects
duncanmcclean Apr 21, 2026
c06f161
Record redirect hits
duncanmcclean Apr 21, 2026
e29c4df
fix namespace of redirect tests
duncanmcclean Apr 21, 2026
9c12b7d
wip
duncanmcclean Apr 21, 2026
30b07c0
Merge branch '7.x' into redirects
duncanmcclean Apr 21, 2026
bc0e7e2
wip
duncanmcclean Apr 21, 2026
c8f6b30
wildcard support
duncanmcclean Apr 21, 2026
007a061
fix failing tests
duncanmcclean Apr 21, 2026
d732a75
use link fieldtype for destination
duncanmcclean Apr 22, 2026
6fbe9a1
rename things
duncanmcclean Apr 22, 2026
4eda3d3
fix failing tests
duncanmcclean Apr 22, 2026
68ae3af
add description to redirects
duncanmcclean Apr 22, 2026
726451c
wip
duncanmcclean Apr 22, 2026
574bc95
change how we generate redirect ids/filenames
duncanmcclean Apr 22, 2026
04d3254
automatic redirects
duncanmcclean Apr 23, 2026
c22eee7
__fixtures__
duncanmcclean Apr 23, 2026
d24269b
track 404 errors
duncanmcclean Apr 23, 2026
1f13c61
wip
duncanmcclean Apr 23, 2026
64b313b
make description optionally listable
duncanmcclean Apr 23, 2026
be2f764
source url fieldtype
duncanmcclean Apr 23, 2026
0d39feb
multi-site support
duncanmcclean Apr 23, 2026
d37d449
empty state
duncanmcclean Apr 23, 2026
adcfb58
enable automatic redirects by default
duncanmcclean Apr 23, 2026
c93a3c5
database redirects
duncanmcclean Apr 23, 2026
13535e9
wip
duncanmcclean Apr 23, 2026
606fc24
database errors
duncanmcclean Apr 23, 2026
d524c40
document database support
duncanmcclean Apr 23, 2026
33f749a
update screenshot
duncanmcclean Apr 23, 2026
3671060
Merge branch '7.x' into redirects
duncanmcclean Apr 23, 2026
7138683
formatting
duncanmcclean Apr 23, 2026
000e8ed
fix duplicate imports
duncanmcclean Apr 23, 2026
b74edd3
disable automatic redirects by default
duncanmcclean Apr 24, 2026
f5cb0fa
fix errors authorization
duncanmcclean Apr 24, 2026
d2ecdbc
call `->command()`, not `->job()`
duncanmcclean Apr 24, 2026
598d14c
wildcard matching should capture single path segments only
duncanmcclean Apr 24, 2026
73318d0
fix search query bypassing site scoping in multi-site
duncanmcclean Apr 24, 2026
b957d28
fix duplicate sentence in error storage docs
duncanmcclean Apr 24, 2026
9ed508a
add last_hit_at to errors stache index
duncanmcclean Apr 24, 2026
08ac7ab
prefix database tables with seo_pro_ to avoid collisions
duncanmcclean Apr 24, 2026
eb894a1
add missing trailing newlines to vue files
duncanmcclean Apr 24, 2026
9776ed8
wip
duncanmcclean Apr 24, 2026
6b01221
refactor multisite hook... so it actually works
duncanmcclean Apr 24, 2026
9bdd54d
reorder
duncanmcclean Apr 24, 2026
c6f69ac
combine
duncanmcclean Apr 24, 2026
ef2d9de
refactor HandleRedirects class
duncanmcclean Apr 24, 2026
7528d49
refactor automatic redirects
duncanmcclean Apr 24, 2026
09d088e
wip
duncanmcclean Apr 24, 2026
9a7c4ce
only show "create redirect" button when user has permission
duncanmcclean Apr 24, 2026
9184c9c
authorize edit/delete actions on redirects listing
duncanmcclean Apr 24, 2026
444c8c1
redirects should be viewable, even without edit permission
duncanmcclean Apr 24, 2026
ac68906
wip
duncanmcclean Apr 24, 2026
7cf1129
Handle 410 (Gone) response code without sending a redirect
duncanmcclean Apr 24, 2026
e9bf538
Add composite indexes on (site, source) and (site, url)
duncanmcclean Apr 24, 2026
38e5531
Filter wildcard redirects at query level instead of in PHP
duncanmcclean Apr 24, 2026
51c2f7e
Rename $redirect to $error in ListedError resource
duncanmcclean Apr 24, 2026
23011c4
Only register AutomaticRedirectSubscriber when enabled
duncanmcclean Apr 24, 2026
43808ea
Remove 410 (Gone) from redirect response codes
duncanmcclean Apr 24, 2026
c17a87d
Restore runtime config checks in AutomaticRedirectSubscriber
duncanmcclean Apr 24, 2026
553fce2
Remove redundant runtime config checks and their tests
duncanmcclean Apr 24, 2026
5db6e37
formatting
duncanmcclean Apr 24, 2026
1116e5d
we only need 301 and 302
duncanmcclean Apr 28, 2026
0a7c733
Simplify help text
jackmcdade Apr 28, 2026
e6c9da8
Build custom publish form for redirects
duncanmcclean Apr 29, 2026
593e4ab
Make the "Enabled" toggle in the sidebar more like entries
duncanmcclean Apr 29, 2026
48e4edf
Add "Test Redirect" button to edit page
duncanmcclean Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,191 @@ You may add a reports widget to your dashboard to get a quick insight into your
],
```

## Redirects

SEO Pro includes a redirect manager that lets you define URL redirects, automatically create redirects when slugs change, and track 404 errors.

### Managing Redirects

Head to `Tools > SEO Pro > Redirects` to create and manage redirects. Each redirect has a source URL, a destination URL, and a response code (301 or 302). Redirects can also be enabled or disabled.

![Redirects listing](https://raw.githubusercontent.com/statamic/seo-pro/refs/heads/7.x/docs-redirects.png)

#### Wildcards

You can use wildcards in your source URLs. Each `*` captures a segment, and you can reference them in the destination with `$1`, `$2`, etc:

- Source: `/blog/*` → Destination: `/articles/$1`
- Source: `/blog/*/posts/*` → Destination: `/articles/$1/entries/$2`

Exact matches always take priority over wildcard matches.

#### Query Strings

By default, query strings from the original URL are retained when redirecting. You may disable this behaviour in your config:

```php
// config/statamic/seo-pro.php

'redirects' => [
'preserve_query_string' => false,
],
```

#### Storage

By default, redirects are stored as YAML files in the `content/seo-pro/redirects` directory. You can change this in the config:

```php
// config/statamic/seo-pro.php

'redirects' => [
'driver' => 'file',
'directory' => base_path('content/seo-pro/redirects'),
],
```

Alternatively, you may store redirects in the database by changing the driver:

```php
// config/statamic/seo-pro.php

'redirects' => [
'driver' => 'database',
],
```

Then run `php please seo-pro:database-redirects` to publish the migration and import existing redirects.

### Multi-Site

Redirects and errors are scoped to individual sites. Each redirect belongs to a single site, and the source URL is stored relative to the site root. For example, a redirect with the source `/about` on the French site will only match requests to `example.com/fr/about` (or `example.fr/about`, depending on your site configuration).

When using the Control Panel, redirects and errors are filtered to the currently selected site. The site can be changed using the site filter in the listing.

When you enable multi-site on an existing install via `php please multisite`, SEO Pro will automatically move your existing redirect and error files into subdirectories for the default site.

### Automatic Redirects

SEO Pro can automatically create redirects when an entry or term's slug changes. This prevents broken links when content is reorganized.

To enable automatic redirects, set the `SEO_PRO_AUTOMATIC_REDIRECTS` environment variable to `true`:

```env
SEO_PRO_AUTOMATIC_REDIRECTS=true
```

By default, automatic redirects apply to all collections and taxonomies. You may limit this to specific collections or taxonomies in the config:

```php
// config/statamic/seo-pro.php

'redirects' => [
'automatic_redirects' => [
'enabled' => env('SEO_PRO_AUTOMATIC_REDIRECTS', false),
'collections' => ['pages', 'posts'],
'taxonomies' => ['tags'],
],
],
```

When an entry's slug changes, a redirect is created from the old URL to the new one, using the default response code (301 by default). If a redirect already exists for the old URL, its destination is updated rather than creating a duplicate.

You may change the default response code in the config:

```php
// config/statamic/seo-pro.php

'redirects' => [
'default_response_code' => 301,
],
```

In a multi-site setup, when an entry's slug changes, redirects are also created for any localizations that share the same slug change, each pointing to the localized version of the content.

Self-referencing redirects (where the source and destination are the same URL) are automatically cleaned up.

### Error Tracking

SEO Pro can track 404 errors, giving you visibility into broken links on your site. When a request doesn't match a redirect, the URL is recorded as an error.

To enable error tracking, set the `SEO_PRO_TRACK_ERRORS` environment variable:

```env
SEO_PRO_TRACK_ERRORS=true
```

Errors can be viewed at `Tools > SEO Pro > Errors`. From there, you can see each error's URL, hit count, and last hit time. Each error also has a link to quickly create a redirect for that URL.

When a redirect is created, any errors matching the redirect's source URL are automatically deleted.

#### Purging Old Errors

It's easy to accumulate lots of errors over time. To keep things tidy, SEO Pro will automatically purge errors older than 30 days.

You may customize the purge threshold in the config:

```php
// config/statamic/seo-pro.php

'redirects' => [
'errors' => [
'purge_after_days' => 30,
],
],
```

You may also run the command manually:

```
php please seo-pro:purge-errors
```

#### Storage

By default, errors are stored in the `storage/statamic/seopro/errors` directory. You can change this in the config:

```php
// config/statamic/seo-pro.php

'redirects' => [
'errors' => [
'driver' => 'file',
'directory' => storage_path('statamic/seopro/errors'),
],
],
```

Errors can be stored in the database independently of redirects:

```php
// config/statamic/seo-pro.php

'redirects' => [
'errors' => [
'driver' => 'database',
],
],
```

Then run `php please seo-pro:database-errors` to publish the migration and import existing errors.

#### Widget

You can add a recent errors widget to your dashboard to see the latest 404s at a glance:

```php
// config/statamic/cp.php

'widgets' => [
[
'type' => 'recent_errors',
'width' => 50,
'limit' => 5,
],
],
```

## Advanced Configuration

### Publishing Config
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
}
},
"require": {
"statamic/cms": "^6.5",
"statamic/cms": "^6.10",
"pixelfear/composer-dist-plugin": "^0.1.6"
},
"require-dev": {
Expand Down
18 changes: 18 additions & 0 deletions config/seo-pro.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,22 @@
],
],

'redirects' => [
'driver' => 'file',
'directory' => base_path('content/seo-pro/redirects'),
'preserve_query_string' => true,
'default_response_code' => 301,
'automatic_redirects' => [
'enabled' => env('SEO_PRO_AUTOMATIC_REDIRECTS', false),
'collections' => ['*'],
'taxonomies' => ['*'],
],
'errors' => [
'enabled' => env('SEO_PRO_TRACK_ERRORS', false),
'driver' => 'file',
'directory' => storage_path('statamic/seopro/errors'),
'purge_after_days' => 30,
],
],

];
Binary file added docs-redirects.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
return [

'humans_txt' => 'Humans.txt',
'redirects' => 'Redirects',
'redirects_description' => 'Manage URL redirects to preserve legacy URLs and improve SEO.',
'errors' => 'Errors',
'errors_description' => 'View 404 errors across your site. Create redirects for the important ones.',
'create_redirect' => 'Create Redirect',
'view_redirects' => 'View Redirects',
'edit_redirects' => 'Edit Redirects',
'create_redirects' => 'Create Redirects',
'delete_redirects' => 'Delete Redirects',
'redirect_source' => 'URL to redirect from. Use `*` as a wildcard, like `/blog/2026/*`.',
'redirect_destination' => 'URL to redirect to. Use `$1`, `$2`, etc. for wildcard matches.',
'redirect_response_code' => 'HTTP response code for this redirect.',
'redirect_description' => 'Optional internal note.',
'reports' => 'Reports',
'report' => 'Report',
'seo_reports' => 'SEO Reports',
Expand Down
2 changes: 2 additions & 0 deletions lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
return [

'json_ld_omit_script_tags' => 'Please omit the script tag. SEO Pro will add it automatically.',
'redirect_url' => 'Must be an absolute URL or a path starting with /.',
'unique_redirect_url' => 'A redirect with this source URL already exists.',

];
43 changes: 43 additions & 0 deletions resources/js/components/fieldtypes/RedirectSourceFieldtype.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup>
import {Fieldtype} from '@statamic/cms';
import { Input } from '@statamic/cms/ui';

const emit = defineEmits(Fieldtype.emits);
const props = defineProps(Fieldtype.props);
const { name, isReadOnly, update, expose } = Fieldtype.use(emit, props);

function onInput(value) {
if (!value) {
return update(value);
}

const siteUrl = props.meta.site_url;

if (siteUrl && value.startsWith(siteUrl)) {
value = value.substring(siteUrl.length);
}

if (!value.startsWith('/')) {
value = '/' + value;
}

update(value);
}

defineExpose(expose);
</script>

<template>
<Input
:model-value="value"
:focus="config.focus"
:read-only="isReadOnly"
:prepend="meta.site_url"
:placeholder="config.placeholder"
:name="name"
:id="id"
@update:model-value="onInput"
@focus="$emit('focus')"
@blur="$emit('blur')"
/>
</template>
37 changes: 37 additions & 0 deletions resources/js/components/redirects/StatusIndicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup>
import { computed } from 'vue';

const props = defineProps({
status: {
type: String,
required: false,
default: 'active',
validator: (value) => ['active', 'inactive'].includes(value),
},
showDot: { type: Boolean, default: true },
showLabel: { type: Boolean, default: false },
});

const statusClass = computed(() => {
if (props.status === 'active') {
return 'bg-green-400';
}

return 'bg-gray-300 dark:bg-gray-200';
});

const label = computed(() => {
const labels = {
active: __('Active'),
inactive: __('Inactive'),
};
return labels[props.status];
});
</script>

<template>
<span class="flex items-center gap-2">
<span v-if="showDot" class="size-2 rounded-full" :class="statusClass" v-tooltip="label" />
<span v-if="showLabel" class="status-index-field select-none" :class="`status-${ status === 'active' ? 'published' : 'draft' }`" v-text="label" />
</span>
</template>
Loading
Loading