Skip to content

Conversation

@jaydrogers
Copy link
Member

@jaydrogers jaydrogers commented Jan 27, 2026

Background

A discussion on r/laravel identified that applications running on serversideup/php images (including those on Laravel Cloud) serve identical content at both /path and /index.php/path. The original poster discovered this because Bing had begun indexing /index.php/... URLs alongside the canonical clean URLs.

They were specifically referencing this line as an issue:

location / {
try_files $uri $uri/ /index.php?$query_string;
}

Where this configuration came from

This configuration has been used internally with our team for over a decade and continues to be in every example that we can find in a Google search for "Laravel FPM NGINX configuration". Few examples:

Further investigation

After running some tests, we found this issue is across all our web server variations:

Affected? Variation
CLI
FPM
⚠️ FPM-NGINX
⚠️ FPM-Apache
⚠️ FrankenPHP

Problem

All three web server variations (fpm-nginx, fpm-apache, frankenphp) allow direct browser and crawler requests to /index.php/some/path to reach PHP, where the framework strips the /index.php prefix and routes the request normally. This means every route in an application has two publicly accessible URLs that return identical content:

This causes three concrete SEO problems:

  1. Duplicate content indexing — search engines index both URLs as separate pages with the same content
  2. Link equity dilution — inbound links and ranking signals are split across two URLs instead of consolidated on one
  3. Crawl budget waste — crawlers spend time re-fetching content they've already indexed under a different URL

Proposed solution

Add a 301 Moved Permanently redirect that intercepts direct requests to /index.php/... and redirects to the clean URL, preserving query strings. This is applied across all three web server variations.

nginx (`fpm-nginx`)

Added to both http.conf.template and https.conf.template:

# Redirect /index.php/... to /... to prevent SEO duplicate content
location ~ ^/index\.php(/.+)$ {
    return 301 $1$is_args$args;
}

The regex ^/index\.php(/.+)$ requires at least one character after /index.php/, so the internal try_files rewrite to bare /index.php is never matched. Normal Laravel routing is completely unaffected.

Apache (`fpm-apache`)

Added to both http.conf and https.conf:

# Redirect /index.php/... to /... to prevent SEO duplicate content
RewriteEngine On
RewriteCond %{THE_REQUEST} \s/index\.php/
RewriteRule ^/index\.php(/.+)$ $1 [R=301,L,QSA]

The RewriteCond matches against %{THE_REQUEST} (the original HTTP request line from the client), which does not change during internal rewrites. This ensures only direct client requests to /index.php/... trigger the redirect, not internal .htaccess processing.

Note: The Apache variation relies on the application's .htaccess file to route clean URLs to index.php (via AllowOverride All). Both Laravel and WordPress ship .htaccess files that handle this by default.

Caddy/FrankenPHP (`frankenphp`)

Added to the `(php-app-common)` snippet in the Caddyfile:

# Redirect /index.php/... to /... to prevent SEO duplicate content
@indexphp path_regexp indexphp ^/index\.php(/.+)$
redir @indexphp {re.indexphp.1} 301

Caddy evaluates redir before php_server in its directive ordering. Caddy also preserves the original query string automatically when the redirect URI does not contain one.

Why 301 redirect (not 404)

  • 301 tells search engines the canonical URL is the clean version. Any existing ranking signals on /index.php/... URLs are transferred to the clean URL. Bookmarks and external links continue to work.
  • 404 would discard any existing ranking signals and break any bookmarks or external links pointing to /index.php/... URLs.

How this affects sites that care about SEO

  • Search engines that have already indexed /index.php/... URLs will follow the 301 and update their index to the clean URL
  • Link equity currently split across duplicate URLs will consolidate on the canonical URL
  • Crawl budget is no longer wasted on duplicate paths
  • No rel=canonical tag or robots.txt rules are needed — the server handles it at the HTTP level, which is the most authoritative signal

How to test

You can test this image using our serversideup/php-dev repository, which automatically builds on push to this PR.

serversideup/php-dev:646-*

View the available testing images →

Further comment

If you support this change, please vote with a 👍 on this PR. If you disagree or have an alternative approach, please vote with a 👎 and comment below with your proposed solution and reasoning.

@jaydrogers jaydrogers changed the base branch from main to release/webserver-improvements-and-fixes January 27, 2026 20:20
@jaydrogers jaydrogers added the 🙏 Help Wanted Issues that specifically could use some help from the community label Jan 27, 2026
@jaydrogers jaydrogers requested a review from danpastori January 27, 2026 20:24
@jaydrogers jaydrogers moved this from Backlog to In review in serversideup/php: 4.4 Release Jan 27, 2026
@sertxudev
Copy link

Hi @jaydrogers ! I'm the one who created the Reddit discussion. I'm going to deploy a Laravel app with the testing image to review the change.

@jaydrogers
Copy link
Member Author

Thanks! Keep me posted of your results.

I did more digging on where this line came from:

location / {
try_files $uri $uri/ /index.php?$query_string;
}

We've had this running for years and your Reddit thread was the first time I was aware of the issue. I'm glad I randomly stumbled upon your discussion.

When I looked through other NGINX examples on how to configure for PHP, every example I could find had the exact same configuration. Then I even noticed it was doing it in Apache and FrankenPHP 🙃

Keep me posted of your results. I reached out to some team members to get their opinion on this too, because merging this will affect all of Laravel Cloud.

I'm really looking forward to getting more community input on what's best because I wouldn't want duplicate SEO content either. We just need to figure out if this fix should be at the "application level" or the "server level" 😃

@sertxudev
Copy link

My Laravel app is live with the php-fpm testing image, and the 301 redirect is working as expected.

Demo URLs:

Let's see if the Laravel Cloud team can confirm we won't break something on their end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🙏 Help Wanted Issues that specifically could use some help from the community

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

3 participants