asignua/filament-seo-files
A Filament plugin for managing SEO-related files such as robots.txt and sitemap.xml directly from your admin panel, making it easy to edit and maintain essential site configuration without touching the filesystem.
Start by registering the plugin in the Filament panel that should expose the SEO files UI, then define the sources that feed sitemap.xml, llms.txt, and llms-full.txt. The first practical use case is a public site with a handful of Eloquent models: add a ModelSource for pages/posts, run php artisan seo-files:sitemap and php artisan seo-files:llms manually, inspect the generated files, then schedule the commands nightly.
After installing or upgrading, run:
php artisan filament:assets
This is required after v1.0.2 because the plugin stylesheet now uses scoped fi-seo-files-* classes and is only linked on the plugin’s own pages. Skipping this step can leave stale generic utility classes that override the panel theme.
Where to look first:
seo-files:sitemap and seo-files:llms for scheduler integration.ModelSource for the common Eloquent-backed source pattern.A good first source is a published Page model with a translatable title and a description field. Once that works, add more sources and restrict locales with ->locales([...]) where needed.
Register sources centrally, usually in a service provider or the package config, and keep them deterministic. ModelSource is the default path for Eloquent models: it can read a translatable title for the file’s language, decode HTML entities in descriptions, apply length limits, and avoid scanning locales it does not serve when restricted with ->locales().
Use the panel buttons for ad hoc generation and the Artisan commands for scheduled runs. Both paths share the same safety model: generation runs outside the admin context, with no Filament panel, tenant, or logged-in user. The scheduler already behaved this way; v1.0.2 makes the panel “Generate” buttons and llms editor match it. Design source queries for a public context. If a global scope depends on auth()->user() or a tenant, it will not see the admin who clicked the button, and it should not leak tenant-specific rows into a global sitemap.xml or llms-full.txt.
For multilingual sites, rely on app()->getLocale() inside source closures. The llms files are built in the language of the file, and ModelSource uses the translatable title for that language. Use ->locales() to limit both output and database scanning. BCP 47 locales such as es-419 and zh-Hant-TW are supported in v1.0.2.
Schedule the commands like any other nightly job:
$schedule->command('seo-files:sitemap')->daily();
$schedule->command('seo-files:llms')->daily();
Both commands take a non-blocking cache lock. If a panel click and the nightly run collide, one run is refused with a non-zero exit code. The panel reports that as a failure, so treat command failures as real operational signals rather than ignoring them.
For tenant-aware applications, do not expect the package to generate tenant-scoped files from the admin panel context. The global files are intentionally built without a tenant. If you need per-tenant outputs, generate them in separate processes or custom jobs where you explicitly establish the tenant context, rather than relying on the panel’s current tenant.
In tests and Octane, the source registry is reset when the service provider registers. A host test suite that boots a fresh application per test will not accumulate duplicate sources, and Octane’s cloned per-request applications keep the sources registered at boot. If you register sources dynamically at runtime, verify that behavior against your boot lifecycle.
Always run php artisan filament:assets after upgrading. The v1.0.2 stylesheet fix is not just cosmetic: generic utilities like .flex, .text-sm, and .text-gray-400 previously beat the panel theme’s responsive and dark: variants. The plugin now scopes its classes and loads the stylesheet only on its own pages.
Generation is deliberately context-free. No Filament panel, tenant, or authenticated user is available while the public files are built, and Filament::auth() is covered by the same guard swap. If your source closures call auth()->user(), Filament::auth(), or a tenant helper, expect null or empty results. This is the intended fix for tenant scopes and user-dependent global scopes leaking into public files.
The cache lock is non-blocking. A refused run exits with an error; the panel shows it as a failure, and it does the same for any non-zero exit code. Check scheduler logs if nightly files stop updating, and avoid manually clearing the lock unless you are sure no generation is running.
For llms output, keep descriptions as plain text or let ModelSource decode entities and no-break spaces before the length limit is applied. Do not double-encode. A non-string title no longer throws, but translatable titles should still be strings for predictable output.
Use ->locales() on ModelSource when a model only has content for some languages. It skips table scans for unsupported languages, which matters on large tables.
The “SEO files” page shows generation time in the app timezone as of v1.0.2. If timestamps look wrong, check config('app.timezone') before assuming the package is at fault.
The “Sitemap URLs” search is case-insensitive on MySQL/MariaDB in v1.0.2. On other databases, verify search behavior if your workflow depends on it.
If you extend the package with custom sources, register them through the same source registry and keep them free of request-specific state. The registry reset on provider registration is there to prevent duplicate sources in test suites and long-running workers; it also means sources registered at boot are the ones Octane expects.
How can I help you explore Laravel packages today?