September 28th, 2026 | AI

By: Justin Phelan

The AI Hallucinated URL That Doesn't 404

One of my client's Ahrefs reports turned up something odd recently. Several AI assistants were recommending their Laravel site and sending visitors to it, which was good news. The links those assistants handed out looked like this:

https://example.com/index.php/services/web-design

The real page lives at /services/web-design. The site has never published a URL with index.php in it; neither its templates nor its sitemap.xml contain one. The assistants invented the prefix on their own.

Most reports of AI assistants hallucinating links end in a 404: the model makes up a plausible path and the visitor lands on an error page. These invented URLs loaded complete, working pages with a 200 status. Nothing looked broken, so the site's 404 monitoring never flagged them.

What the page looked like

Before we fixed it, the /index.php/ version of every page on the production site returned the full page with a 200. Its <link rel="canonical"> tag pointed at the /index.php/ URL, so the page named the wrong address as the correct one. And the page carried 157 internal links, every one of them with the /index.php/ prefix.

Those links are what turned a stray URL into a real problem. A visitor or a crawler who arrived at one invented URL could click through the entire site and never leave the duplicate. One hallucinated link exposed a full second copy of the website, with its own canonical tags vouching for it.

The clean URLs were fine. Correct canonicals, no prefixed links. The duplicate only existed for requests that started with /index.php.

Why Laravel serves it

Laravel handles incoming requests with Symfony's HttpFoundation Request class. When the request URI begins with the name of the front controller script (/index.php), Request::prepareBaseUrl()treats that segment as the application's base URL, the same way it would if the app were installed in a subfolder.

From then on, every URL Laravel generates carries that base. url(), route(), url()->current(), redirects: all of them start producing /index.php/... addresses. Canonical tags and navigation menus are built from those helpers, so they follow along.

This is deliberate. The behavior exists to support apps installed in subdirectories and servers that don't rewrite URLs, where /index.php/some/page was historically the only format that worked. Symfony's own development front controller used /app_dev.php/... URLs for years.

The server side is deliberate too. Laravel's documented nginx configuration includes a location ~ ^/index\.php(/|$) block, which sends these URLs straight to PHP. On the client's Forge server, /index.php/services/web-design reached Laravel with the original request URI intact, and Laravel did exactly what it was built to do.

Apache setups don't catch it either. Laravel's default public/.htaccess redirects trailing slashes but has no rule for index.php.

An old PHP problem with a new source

Duplicate content from index.php URLs is an old complaint. Joomla, Magento and CodeIgniter all have years of documentation and forum threads on stripping index.php from URLs and 301-redirecting the old ones. Drupal sites in the Drupal 6 and 7 era leaned on the Global Redirect module for the same job, and Lullabot's old writeup on Drupal duplicate content walks through it.

In every one of those cases, the site created the duplicates itself. With rewriting off or misconfigured, the CMS put index.php into its own links, and search engines indexed what they found. Search engines index URLs they discover. They don't make them up.

That's why we stopped worrying about it. On a modern PHP site with working rewrites, the /index.php/variants exist in theory and nowhere in practice. Nothing links to them, so nothing crawls or indexes them. Ignoring them was a reasonable call for a long time.

What changed is that an outside party now generates URLs your site never published. An AI assistant writes a plausible address and the framework serves it, so the variant exists in the wild without anyone on your side creating it.

We don't know for certain why models add the prefix. The pattern is everywhere in their training data, though. WordPress has documented "PATHINFO" permalinks for years, which it calls "almost pretty" URLs, in the form /index.php/yyyy/mm/dd/post-name/. Add two decades of PHP tutorials, forum answers and old CodeIgniter and Magento sites, and a model has seen /index.php/ in front of a clean path an enormous number of times. To a model, it looks like a legitimate way to address a PHP page. On a stock Laravel install, it is one.

The research counts 404s

The two main studies on hallucinated links measure one outcome: a page that doesn't exist.

Ahrefs looked at 16 million URLs in a study published in September 2025. AI assistants sent visitors to 404 pages 2.87 times as often as Google Search did. ChatGPT had the highest rate, with 1.01% of clicked URLs and 2.38% of cited URLs returning a 404. Ahrefs noted that the invented URLs tend to fit the site's existing patterns; on its own blog, assistants made up paths like /blog/internal-links/ and /blog/newsletter/. The recommendation was to 301 any hallucinated URL that gets meaningful traffic.

SE Ranking ran a similar study in December 2025 on 145,463 URLs cited by ChatGPT. 97.55% returned 200 and 1.22% returned 404, compared with 0.56% for Google's AI Overviews.

Both studies treat a 200 as a success. That's a sensible default, and it means an /index.php/ duplicate counts as a good citation in both datasets. A hallucinated URL that works falls outside what these studies measure, and we couldn't find any writing on AI assistants producing working URL variants like this one.

Is this only a Laravel problem?

Laravel, Drupal 8 and later, and Symfony itself all build requests with the same HttpFoundation Requestclass. We ran that class directly against a test request: with SCRIPT_NAME set to /index.php, a request for /index.php/services/web-design comes back with a base URL of /index.php and a path of /services/web-design. Every application built on this class starts from there. What happens next depends on the application, so we tested the other platforms we maintain.

Four Drupal sites, two on Drupal 10 and two on Drupal 11, all 301-redirected /index.php/ URLs to the clean path. Drupal core doesn't do that on its own. The redirect comes from the contributed Redirect module, whose "Enforce clean and canonical URLs" setting is on by default and carries on the job Global Redirect did in Drupal 7. A Drupal site without Redirect needs its own answer, and there are now two small contrib modules for Drupal 10 and 11, Index.php Redirect and Clean Index PHP URL, that exist only to redirect /index.php/ paths. The second one names externally generated links as a reason it exists.

Statamic runs on Laravel, but the Statamic sites we tested returned a 404 for the prefixed URLs. No duplicate gets served. A visitor following an AI-invented link still lands on an error page, which puts Statamic back in the ordinary hallucinated-link case the research measures. If you've added your own routes in routes/web.php alongside Statamic, test one of those as well, since they go through Laravel's router.

WordPress redirected too. We tested two fresh installs, one local and one on a live host, and both sent /index.php/ URLs to the clean path. Neither had plugins doing the work, so the redirect seems to come from WordPress itself.

Joomla, CakePHP and CodeIgniter each handle routing their own way. We haven't tested their current defaults, so we won't make claims about them. /index.php/path URLs have been a supported format in plenty of PHP software over the years, which is reason enough to run the thirty-second check at the end of this post on any PHP site.

What are the costs?

The damage is modest, and still worth fixing.

An AI assistant citing a URL does nothing to your Google rankings. Google doesn't penalize duplicate content either; it picks a canonical URL and groups the duplicates under it.

The risk comes from how these URLs spread. AI answers get pasted into forum posts, AI-written articles, shared chats and documentation. Each copy becomes an ordinary link that any crawler can follow. Ahrefs pointed out that its own crawler fetches hallucinated URLs when they appear in published AI-generated content, and SE Ranking found nonexistent URLs on its own site that had picked up backlinks. Once a crawler lands on a working /index.php/ page with a self-referencing canonical and 157 prefixed internal links, it can crawl the whole duplicate site. The realistic outcome is wasted crawl budget and the occasional wrong URL showing up in results.

The loop can also feed itself. ChatGPT's search is widely reported to draw heavily on Bing's index. If Bing indexes the variants, ChatGPT has more reason to cite them, and the citations spread further.

Links split too. A real backlink pointing at an /index.php/ URL builds value on a duplicate instead of your page. A 301 passes that value along.

Analytics split along the same lines. GA4, Ahrefs and Search Console all report the variant as a separate page, so a page's traffic gets divided between two rows. Any Google Tag Manager, Google Ads or Meta rule that matches an exact page path will miss visits to the variant. Rules using "contains" still fire.

For a marketing site, that last one might be the most expensive. A conversion tag that silently stops firing on a slice of your AI-referred traffic is hard to notice and easy to fix.

The fix

We added a global middleware at the front of the Laravel stack. It 301-redirects any GET or HEAD request that arrives with a base URL to the same path and query string without it. Other methods, like form POSTs, pass through untouched, so a stray form submission doesn't get turned into a GET.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;

class RemoveIndexPhpFromUrl
{
    public function handle(Request $request, Closure $next): Response
    {
        $baseUrl = $request->getBaseUrl();

        if ($baseUrl === '' || ! in_array($request->getMethod(), ['GET', 'HEAD'], true)) {
            return $next($request);
        }

        $pathAndQuery = substr($request->getRequestUri(), strlen($baseUrl));

        if ($pathAndQuery === '' || $pathAndQuery[0] !== '/') {
            $pathAndQuery = '/'.$pathAndQuery;
        }

        return new RedirectResponse($request->getSchemeAndHttpHost().$pathAndQuery, 301);
    }
}

Register it in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->prepend(RemoveIndexPhpFromUrl::class);
})

With that in place, /index.php/services/web-design?utm_source=chatgpt.com redirects to /services/web-design?utm_source=chatgpt.com and a bare /index.php goes to /. Clean URLs never touch the redirect. It's covered by Pest tests on the client project.

One gotcha cost us some time. You can't build the redirect target with redirect('/path') or url(). Both helpers read the same base URL you're trying to remove, so they add /index.php straight back on and you get a redirect loop. Build the absolute URL from getSchemeAndHttpHost() and the raw request URI instead, as above.

This middleware assumes the app is served from the web root. It treats any non-empty base URL as the /index.php prefix. If your app really does live in a subdirectory, the check needs to compare against /index.php specifically.

We chose middleware over a server rule because it lives in git and behaves the same on every environment, from a developer's laptop to production. An nginx rewrite or a Cloudflare redirect rule would also work. We didn't write or test one for this project, so we're not publishing one here. If you go that route, test it against query strings and the bare /index.php case before it reaches production.

Check your own sites

Pick any real page on a PHP site you maintain and run:

curl -s -o /dev/null -w "%{http_code}\n" https://example.com/index.php/any-real-page

A 301 or a 404 means you're fine. A 200 means the duplicate exists; open that URL in a browser, view the source, and check where the canonical tag and the navigation links point. If they carry the /index.php/ prefix, you have the same full-site copy our client had, and the only thing keeping it out of a search index is whether an AI assistant has guessed it and someone has published the guess.

Justin Phelan

Full Stack Developer

Let's make something great together.