# Slug Redirect Duplicate Prevention

**Last Updated:** 2026-03-17

## Problem

When a blog post slug is renamed (e.g., year-specific `2025-gastronomie-mindestlohn` to evergreen `gastronomie-mindestlohn`), the old JSON file may still exist on production servers if deployment does not delete removed files. This causes duplicate post cards on index and category pages (`/insights/`, `/insights/ratgeber/`), with the old card linking to a URL that 301-redirects to the canonical one.

## Solution

### 1. Slug Redirect Config

`v2/config/blog-slug-redirects.php` maps deprecated slugs to their canonical slugs:

```php
return [
    '2025-gastronomie-mindestlohn' => 'gastronomie-mindestlohn',
];
```

This config is the single source of truth for slug redirects. It is used by:

- `v2/config/blog-template-helpers.php` – to skip deprecated slugs when loading posts
- `router.php` – for 301 redirects on the PHP built-in server
- `.htaccess` – for 301 redirects in production (add rules when adding new mappings)

### 2. Filtering in Post Loaders

Both `load_blog_posts_by_category()` and `load_blog_post_summaries_by_category()` filter out deprecated slugs:

```php
// Load slug redirect mappings (deprecated → canonical) - skip deprecated slugs
$slug_redirects = [];
$slug_redirects_file = __DIR__ . '/blog-slug-redirects.php';
if (file_exists($slug_redirects_file)) {
    $slug_redirects_cfg = @include $slug_redirects_file;
    if (is_array($slug_redirects_cfg)) {
        $slug_redirects = $slug_redirects_cfg;
    }
}

foreach ($files as $file) {
    $slug = basename($file, '.json');
    // ...
    if (isset($slug_redirects[$slug])) {
        continue; // Deprecated slug - canonical version will be loaded
    }
    $post_data = load_blog_post($cat, $slug);
    // ...
}
```

### 3. Adding a New Slug Redirect

When renaming a post slug (e.g., year-specific to evergreen):

1. Add the mapping to `v2/config/blog-slug-redirects.php`
2. Add the corresponding RewriteRule to `.htaccess` (see existing `2025-gastronomie-mindestlohn` rule)
3. Update `router.php` – it loads from the config automatically
4. Rename the JSON file in the repo (e.g., `2025-gastronomie-mindestlohn.json` → `gastronomie-mindestlohn.json`)
5. Update internal links in other posts to use the canonical URL
6. Deploy; the code fix ensures no duplicate cards even if the old file remains on production
7. Optionally remove the stale file from production: `rm v2/data/blog/posts/ratgeber/2025-gastronomie-mindestlohn.json`

## Deployment Cleanup

If the deprecated JSON file still exists on production after deployment:

```bash
# On production server
rm -f /path/to/v2/data/blog/posts/ratgeber/2025-gastronomie-mindestlohn.json
```

Or ensure deployment uses `rsync --delete` (or equivalent) so removed repo files are deleted on the server.

## Related

- `docs/systems/landing-page-redirects/UMLAUT_DUPLICATE_PREVENTION.md` – Similar pattern for umlaut → ASCII slug migration
- `v2/config/blog-slug-redirects.php` – Config file
- `docs/content/blog/posts/ratgeber/gastronomie-mindestlohn/SLUG_CHANGE_CHECKLIST.json` – Example slug change documentation
