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.
- Installation
- Quick Start
- Building Your Document
- Configuration
- Routes
- Localization
- Static File Generation
- llms-full.txt
- Caching
- Output Format
- Testing
- License
composer require schaefersoft/laravel-llms-txtPublish the configuration file:
php artisan vendor:publish --tag=llms-txt-configThat's it. The package is auto-discovered by Laravel, and /llms.txt is available immediately.
The package works in two modes:
-
Zero-config — Without any setup,
/llms.txtis automatically generated from all registeredGETroutes in your application. Internal routes (Telescope, Horizon, Debugbar) and routes with URI parameters are excluded. Additional routes can be excluded via theexclude_routesconfig option. -
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 hostingTip: For larger projects, consider a dedicated
LlmsTxtServiceProviderto keep yourAppServiceProviderclean.
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 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);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()andmake()are identical — use whichever you prefer.
Note: The
entry()shorthand returns theSection, not theEntry. To call methods on theEntryitself (e.g.description()), useEntry::create()withaddEntry()— or simply pass the description as the third argument toentry().
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),
))
)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')
)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. |
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).
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
],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.
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',
],
];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.
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();
}
);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 --fullBy 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.
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();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();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 --fullIf 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.
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:clearNote:
php artisan llms:generateflushes the cache automatically after a successful run, so the dynamic routes immediately reflect the new content.
# Site Title
> Site description
## Section Name
- [Entry Title](https://example.com): Entry description
- [Another Entry](https://example.com/other)Same structure as llms.txt, but with the fetched HTML/text content of each URL appended below its entry.
composer testMIT — see LICENSE.