schaefersoft/laravel-llms-txt

Automatically generate llms.txt and llms-full.txt for Laravel applications

Maintainers

Package info

github.com/schaefersoft/laravel-llms-txt

pkg:composer/schaefersoft/laravel-llms-txt

Transparency log

Statistics

Installs: 1 897

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 0

1.2.0 2026-07-02 17:41 UTC

This package is auto-updated.

Last update: 2026-07-02 17:41:31 UTC


README

Latest Version on Packagist Tests Total Downloads

Automatically generate llms.txt and llms-full.txt files for your Laravel application — helping AI models understand your website, just like sitemap.xml helps search engines.

Built according to the llmstxt.org specification.

Requirements: PHP 8.2+ and Laravel 10, 11, 12, or 13.

Table of Contents

Installation

composer require schaefersoft/laravel-llms-txt

Publish the configuration file:

php artisan vendor:publish --tag=llms-txt-config

That's it. The package is auto-discovered by Laravel, and /llms.txt is available immediately.

Quick Start

The package works in two modes:

  1. Zero-config — Without any setup, /llms.txt is automatically generated from all registered GET routes in your application. Internal routes (Telescope, Horizon, Debugbar) and routes with URI parameters are excluded. Additional routes can be excluded via the exclude_routes config option.

  2. Custom definition (recommended) — Register a configure callback to have full control over the output. It always takes precedence over auto-generation.

// app/Providers/AppServiceProvider.php

use SchaeferSoft\LaravelLlmsTxt\LlmsTxt;

public function boot(): void
{
    LlmsTxt::configure(fn ($llms) => $llms
        ->title('My App')
        ->description('A short description of what this site offers.')
        ->section('Services', fn ($s) => $s
            ->entry('Web Development', 'https://example.com/web', 'Laravel & Vue.js')
            ->entry('Hosting', 'https://example.com/hosting', 'Managed hosting')
        )
    );
}

This produces:

# My App

> A short description of what this site offers.

## Services
- [Web Development](https://example.com/web): Laravel & Vue.js
- [Hosting](https://example.com/hosting): Managed hosting

Tip: For larger projects, consider a dedicated LlmsTxtServiceProvider to keep your AppServiceProvider clean.

Building Your Document

Title and Description

Every document starts with a title and an optional description:

LlmsTxt::make()
    ->title('My App')
    ->description('Short tagline for the site.');

Both methods accept a string or a Closure for lazy evaluation (useful for localization):

LlmsTxt::make()
    ->title(fn () => __('llms.title'))
    ->description(fn () => __('llms.description'));

Sections and Entries

Sections group related entries under a heading. Entries are links with a title, URL, and optional description.

Verbose style — useful when you need to store references to entries:

use SchaeferSoft\LaravelLlmsTxt\Entry;
use SchaeferSoft\LaravelLlmsTxt\Section;

$section = Section::create('Services')
    ->addEntry(Entry::create('Web Development', 'https://example.com/web', 'Laravel & Vue.js'))
    ->addEntry(Entry::create('Hosting', 'https://example.com/hosting'));

LlmsTxt::make()
    ->title('My App')
    ->addSection($section);

Fluent Shorthand

For a more compact style, use section() and entry() directly on the builder:

LlmsTxt::make()
    ->title('My App')
    ->section('Services', fn ($s) => $s
        ->entry('Web Development', 'https://example.com/web', 'Laravel & Vue.js')
        ->entry('Hosting', 'https://example.com/hosting', 'Managed hosting')
    )
    ->section('References', fn ($s) => $s
        ->entry('Projects', 'https://example.com/references', 'All client projects')
    );

create() and make() are identical — use whichever you prefer.

Note: The entry() shorthand returns the Section, not the Entry. To call methods on the Entry itself (e.g. description()), use Entry::create() with addEntry() — or simply pass the description as the third argument to entry().

Model-Based Entries

Use entries() on a section to map a collection of models (or any iterable) into entries. The callback receives each item and must return an Entry instance.

LlmsTxt::configure(fn ($llms) => $llms
    ->title('My App')
    ->section('Services', fn ($s) => $s
        ->entries(Service::published()->get(), fn ($service) => Entry::create(
            $service->name,
            route('services.show', $service),
            $service->tagline,
        ))
    )
);

You can combine entry() and entries() freely in the same section:

->section('Blog', fn ($s) => $s
    ->entry('All Posts', route('blog.index'))
    ->entries(Post::featured()->get(), fn ($post) => Entry::create(
        $post->title,
        route('blog.show', $post),
    ))
)

Conditional Content

Both LlmsTxt and Section support a when() method that mirrors Laravel's own when():

LlmsTxt::make()
    ->title('My App')
    ->section('Services', fn ($s) => $s
        ->entry('Web Development', 'https://example.com/web')
        ->when((bool) config('features.shop'), fn ($s) => $s
            ->entry('Shop', 'https://example.com/shop')
        )
    )
    ->when((bool) config('features.api'), fn ($llms) => $llms
        ->section('API', fn ($s) => $s
            ->entry('API Docs', 'https://example.com/api')
        )
    );

You can also pass a Closure as the condition for lazy evaluation:

->when(fn () => Feature::active('shop'), fn ($s) => $s
    ->entry('Shop', 'https://example.com/shop')
)

Configuration

After publishing, the config file is at config/llms-txt.php:

Option Default Description
route_enabled true Enable or disable the HTTP routes entirely.
llms_txt_route '/llms.txt' URL path for the standard file.
llms_full_txt_route '/llms-full.txt' URL path for the full file.
full_route_enabled false Serve llms-full.txt dynamically. Disabled by default — see llms-full.txt.
register_routes true Auto-register routes. Set to false for manual registration.
cache_enabled true Cache rendered output.
cache_ttl 3600 Cache lifetime in seconds.
disk null Output location for static files. null writes directly into the public/ folder; set a disk name to use a filesystem disk.
exclude_routes [] URI or route-name patterns (with * wildcards) to exclude from auto-generation.
locales [] List of supported locales (e.g. ['en', 'de']).
localize_routes false Register locale-prefixed routes like /en/llms.txt.

Routes

Automatic Route Registration

By default, the package registers one route:

Route Description
GET /llms.txt Serves the standard llms.txt output.

A second route, GET /llms-full.txt, is only registered when full_route_enabled is set to true — see llms-full.txt for why it is opt-in.

Routes are registered automatically when register_routes is true (the default).

Excluding Routes from Auto-Generation

In zero-config mode the document is built from all registered GET routes. Use exclude_routes to keep specific routes out of the output. Patterns are matched against both the route URI and the route name, and support the * wildcard:

// config/llms-txt.php
'exclude_routes' => [
    'admin/*',        // URI wildcard: excludes /admin and everything below
    'legal/imprint',  // exact URI
    'internal.*',     // route-name wildcard
],

Manual Route Registration

To apply custom middleware or headers, disable automatic registration and call LlmsTxt::routes() yourself:

// config/llms-txt.php
'register_routes' => false,
// routes/web.php
use SchaeferSoft\LaravelLlmsTxt\LlmsTxt;

Route::middleware(['web', 'cache.headers:public;max_age=3600'])
    ->group(function () {
        LlmsTxt::routes();
    });

LlmsTxt::routes() is idempotent — it is safe to call more than once.

Localization

Translating Content

A single configure callback covers all locales. The package sets the application locale before invoking your callback, so __() and route() return the correct values automatically.

LlmsTxt::configure(fn ($llms) => $llms
    ->title(__('llms.title'))
    ->description(__('llms.description'))
    ->section(__('llms.sections.services'), fn ($s) => $s
        ->entry(
            __('llms.entries.web_dev'),
            route('services.web'),
            __('llms.entries.web_dev_desc'),
        )
    )
);

Create your language files as usual:

// lang/en/llms.php
return [
    'title'       => 'My App',
    'description' => 'Software agency',
    'sections'    => ['services' => 'Services'],
    'entries'     => [
        'web_dev'      => 'Web Development',
        'web_dev_desc' => 'Modern web applications with Laravel & Vue.js',
    ],
];
// lang/de/llms.php
return [
    'title'       => 'Meine App',
    'description' => 'Software-Agentur',
    'sections'    => ['services' => 'Leistungen'],
    'entries'     => [
        'web_dev'      => 'Webentwicklung',
        'web_dev_desc' => 'Moderne Webanwendungen mit Laravel & Vue.js',
    ],
];

Locale-Prefixed Routes

Enable locale-prefixed routes to serve translated versions at /en/llms.txt, /de/llms.txt, etc.:

// config/llms-txt.php
'locales'          => ['en', 'de'],
'localize_routes'  => true,

Unknown locale segments return a 404 response.

Using mcamara/laravel-localization

If you use mcamara/laravel-localization, disable automatic registration and wrap the routes in a localized group:

// config/llms-txt.php
'register_routes' => false,
// routes/web.php
Route::group(
    ['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['localize']],
    function () {
        LlmsTxt::routes();
    }
);

Static File Generation

Artisan Command

Generate static files with the llms:generate command:

# Generate public/llms.txt
php artisan llms:generate

# Also generate public/llms-full.txt
php artisan llms:generate --full

# Generate for a specific locale (e.g. public/de/llms.txt)
php artisan llms:generate --locale=de

# Generate for all configured locales
php artisan llms:generate --all-locales

# Combine flags
php artisan llms:generate --all-locales --full

By default, files are written directly into your application's public/ folder, so public/llms.txt is immediately served at https://your-app.test/llms.txt. To write to a filesystem disk instead (e.g. s3), set the disk config option.

Programmatic Export

You can also write files to disk from code:

// Write to the default location
LlmsTxt::make()->title('My App')->writeToDisk();

// Write to a custom path
LlmsTxt::make()->title('My App')->writeToDisk('custom/path/llms.txt');

// Write with a locale prefix (writes to de/llms.txt)
LlmsTxt::make()->title('My App')->locale('de')->writeToDisk();

// Write the full version
LlmsTxt::make()->title('My App')->writeFullToDisk();

llms-full.txt

The full variant fetches the content of each entry URL and appends it below the entry. URLs that cannot be fetched are silently skipped.

// Render as a string
$content = LlmsTxt::make()
    ->title('My App')
    ->section('Docs', fn ($s) => $s
        ->entry('API', 'https://example.com/api')
    )
    ->renderFull();

// Write directly to disk
LlmsTxt::make()->title('My App')->writeFullToDisk();

Serving llms-full.txt Dynamically

The /llms-full.txt route is disabled by default: on a cache miss it performs an HTTP request to every entry URL — which is slow, usually calls back into your own application, and lets any visitor trigger outbound traffic.

The recommended approach is generating a static file instead:

php artisan llms:generate --full

If you understand the trade-offs and want the dynamic route anyway, enable it explicitly:

// config/llms-txt.php
'full_route_enabled' => true,

With cache_enabled => true (the default), the fetched content is cached for cache_ttl seconds, so the entry URLs are only fetched once per TTL.

Caching

When cache_enabled is true (the default), rendered output is cached for the duration specified by cache_ttl.

// Use the default cache key ('llms-txt')
$content = LlmsTxt::make()->title('My App')->getCached();

// Use a custom cache key
$content = LlmsTxt::make()->title('My App')->getCached('my-custom-key');

// Flush ALL package cache keys (base keys + locale variants used by the routes)
LlmsTxt::make()->flushCache();

// Flush a single custom key
LlmsTxt::make()->flushCache('my-custom-key');

You can also clear the cache from the command line:

php artisan llms:clear

Note: php artisan llms:generate flushes the cache automatically after a successful run, so the dynamic routes immediately reflect the new content.

Output Format

llms.txt

# Site Title

> Site description

## Section Name
- [Entry Title](https://example.com): Entry description
- [Another Entry](https://example.com/other)

llms-full.txt

Same structure as llms.txt, but with the fetched HTML/text content of each URL appended below its entry.

Testing

composer test

License

MIT — see LICENSE.