Fix duplicate SEO issues with Apache, NGINX, and FrankenPHP#646
Fix duplicate SEO issues with Apache, NGINX, and FrankenPHP#646jaydrogers wants to merge 1 commit intorelease/webserver-improvements-and-fixesfrom
Conversation
|
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. |
|
Thanks! Keep me posted of your results. I did more digging on where this line came from: 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" 😃 |
|
My Laravel app is live with the Demo URLs:
Let's see if the Laravel Cloud team can confirm we won't break something on their end. |
Quick updateI spoke to Chris Fidao (one of the team leads @ Laravel Cloud) and he said this redirect should not be an issue for Laravel Cloud. I also chatted with Eric Barnes @ Laravel News and he was nice enough to spend time looking back at his Forge config. From he saw, it looks like Forge had the same config and did not redirect either. He thinks having a redirect is a good idea too. Next Steps for mergingI think this is an important piece to implement for serversideup/php. We stride to be "batteries included" and "production-ready". I have a feeling this NGINX config is how almost every major service with PHP has been running over the last decade, but it wasn't until @sertxudev raised an issue where we saw the unintended consequences of supporting "/my-page" and "/index.php/my-page" URLs. In 2026, I don't see any reason where someone would want to have "/index.php/my-page" indexed by a search engine. 👉 The only thing holding up this merge today: Workaround for the time beingIf someone needs an immediate solution today, I'd highly recommend setting a Canonical Tag on your page specifying which URL you'd like indexed by Google: https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls You'll see this merged once I return, but please add any additional thoughts before if you see and room for improvement. I greatly appreciate the community's help on this! |
Background
A discussion on r/laravel identified that applications running on serversideup/php images (including those on Laravel Cloud) serve identical content at both
/pathand/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:
docker-php/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template
Lines 35 to 37 in 6c8c8ca
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:
Problem
All three web server variations (
fpm-nginx,fpm-apache,frankenphp) allow direct browser and crawler requests to/index.php/some/pathto reach PHP, where the framework strips the/index.phpprefix 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:
Proposed solution
Add a
301 Moved Permanentlyredirect 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.templateandhttps.conf.template:The regex
^/index\.php(/.+)$requires at least one character after/index.php/, so the internaltry_filesrewrite to bare/index.phpis never matched. Normal Laravel routing is completely unaffected.Apache (`fpm-apache`)
Added to both
http.confandhttps.conf:The
RewriteCondmatches 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.htaccessprocessing.Note: The Apache variation relies on the application's
.htaccessfile to route clean URLs toindex.php(viaAllowOverride All). Both Laravel and WordPress ship.htaccessfiles that handle this by default.Caddy/FrankenPHP (`frankenphp`)
Added to the `(php-app-common)` snippet in the Caddyfile:
Caddy evaluates
redirbeforephp_serverin its directive ordering. Caddy also preserves the original query string automatically when the redirect URI does not contain one.Why 301 redirect (not 404)
/index.php/...URLs are transferred to the clean URL. Bookmarks and external links continue to work./index.php/...URLs.How this affects sites that care about SEO
/index.php/...URLs will follow the 301 and update their index to the clean URLrel=canonicaltag orrobots.txtrules are needed — the server handles it at the HTTP level, which is the most authoritative signalHow to test
You can test this image using our serversideup/php-dev repository, which automatically builds on push to this PR.
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.