lintpage
~/llms-txt/drupal
§ Drupal

llms.txt on Drupal

Three ways to serve /llms.txt from Drupal, why a file in the docroot can vanish on your next Composer deploy, and the .htaccess rule that blocks it on some hosts.

Drupal sits behind a front controller in the docroot (web/ or docroot/). A real file there is served directly; anything generated needs a route.

§ how to serve it

Serving /llms.txt from Drupal.

step 1

The quick way: a file in the docroot

Drupal's docroot is web/ on a Composer-managed site and docroot/ on some hosting platforms - the directory containing index.php and core/. A file placed there is served by the web server without Drupal bootstrapping at all.

bash
your-project/
  composer.json
  vendor/
  web/                  ←  the docroot
    core/
    modules/
    index.php
    robots.txt
    llms.txt            ←  here, beside robots.txt
step 2

The durable way: a route in a custom module

A route survives deploys, lives in version control, and can build the file from your actual content. This is the approach to use on anything managed by Composer, because the docroot is partly rebuilt on deploy.

modules/custom/acme_llms/acme_llms.routing.yml
acme_llms.txt:
  path: '/llms.txt'
  defaults:
    _controller: '\Drupal\acme_llms\Controller\LlmsTxtController::build'
  requirements:
    _access: 'TRUE'
step 3

The controller that renders it

Return a plain Response, not a render array - a render array gets wrapped in your theme, which is how a correct implementation ends up serving HTML. Set the content type explicitly and add cache metadata so Drupal's page cache and any reverse proxy treat it properly.

modules/custom/acme_llms/src/Controller/LlmsTxtController.php
<?php

namespace Drupal\acme_llms\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Response;

class LlmsTxtController extends ControllerBase {

  public function build(): Response {
    $lines = [
      '# ' . $this->config('system.site')->get('name'),
      '',
      '> ' . $this->config('system.site')->get('slogan'),
      '',
      '## Docs',
      '',
    ];

    // Build the link list from your own content query here.

    $response = new Response(implode("\n", $lines) . "\n");
    $response->headers->set('Content-Type', 'text/plain; charset=utf-8');
    return $response;
  }

}
§ what goes wrong here

What Drupal gets wrong.

The failures below are specific to Drupal. A generic llms.txt guide will not mention any of them.

01

Composer rebuilds the docroot and drops your file

The Drupal-specific trap. On a Composer-managed site, drupal/core-composer-scaffold owns files in the docroot and re-creates them on install and update. A hand-placed llms.txt is not tracked by the scaffold, so depending on your deploy pipeline it either survives by luck or disappears on the next build. If you want a file there, add it to the scaffold's file-mapping in composer.json so it is written deliberately.

composer.json
{
  "extra": {
    "drupal-scaffold": {
      "file-mapping": {
        "[web-root]/llms.txt": "assets/llms.txt"
      }
    }
  }
}
02

A render array instead of a Response

Returning an array from the controller hands the output to Drupal's render pipeline, which wraps it in the active theme. The response is then a full HTML page served at /llms.txt with a 200 - valid-looking to anything that only checks the status code, and useless to anything that reads the body.

03

.htaccess rules that block unfamiliar extensions

Drupal ships a fairly strict .htaccess, and hardened hosting adds to it. Some configurations only allow a known list of static file extensions through to the filesystem. If your file is definitely in the docroot and still 404s, check .htaccess before assuming Drupal is at fault.

04

The internal page cache serving a stale copy

With Internal Page Cache enabled, a route-based llms.txt is cached like any other anonymous response. After changing the content, flush caches - otherwise you are validating yesterday's file.

§ faq

Questions, answered.

Where do I put llms.txt in Drupal?
In the docroot - web/ on a Composer-managed site, docroot/ on some hosts - beside index.php and robots.txt. On a Composer site, add it to the scaffold file-mapping in composer.json as well, or a future composer install may rebuild the docroot without it.
Is there a Drupal module for llms.txt?
Contributed modules have appeared, and their maturity varies. A custom route and controller is about thirty lines and gives you exactly the file you want, which for something this simple is usually the better trade than a dependency you have to evaluate and keep updated.
Why does my Drupal llms.txt render with my theme around it?
Your controller is returning a render array rather than a Response object. A render array goes through Drupal's render pipeline, which applies the active theme and produces a full HTML page. Return a Symfony Response with Content-Type: text/plain instead.
Does llms.txt need to be in the Drupal sitemap?
No. llms.txt is fetched at a known path by clients that already know to look for it, the same way robots.txt is. Adding it to your XML sitemap does nothing useful and puts a non-HTML file in a list meant for indexable pages.
§ the part that matters

An llms.txt will not fix a site AI cannot read.

Before writing an index for AI clients, check that they can fetch your pages at all. LintPage runs 60 checks against a URL in about 30 seconds - free, no signup.

run a full scan →