devonab/filament-easy-footer
Filament EasyFooter adds a flexible, customizable footer to FilamentPHP admin panels (v3–v5). Seamlessly integrate a polished footer and configure enable/disable, position, custom sentence, GitHub/installed versions, load time, logo links, and custom themes via simple options.
Install (match the Filament major version first):
| FilamentPHP | Package version |
|---|---|
| v3.x | ^1.0 |
| v4.x / v5.x | ^2.0 |
composer require devonab/filament-easy-footer:^2.0
php artisan vendor:publish --tag="filament-easy-footer-config"
Register in the panel provider — this is the only mandatory step:
use Devonab\FilamentEasyFooter\EasyFooterPlugin;
->plugins([
EasyFooterPlugin::make(),
])
Where to look first:
config/filament-easy-footer.php — only three things live here: app_name, github.*, and (later versions) versioning.local_fallback_config_key. Everything else is fluent chained off the plugin.->plugins([...]) block — the whole API is a builder: withSentence(), withLinks(), withLogo(), withFooterPosition(), etc.--tag="filament-easy-footer-views") if you need markup changes beyond what the fluent API exposes.First use case — the 10-minute win: a branded footer with copyright, three legal links, and a border, without touching Blade:
EasyFooterPlugin::make()
->withSentence('© ' . date('Y') . ' Acme Inc.')
->withLinks([
['title' => 'About', 'url' => 'https://acme.test/about'],
['title' => 'CGV', 'url' => 'https://acme.test/cgv'],
['title' => 'Privacy', 'url' => 'https://acme.test/privacy'],
])
->withBorder(),
Because the plugin hooks into panels::footer (or a sidebar hook), the footer appears on every panel page — including login/register — the moment you register it. That is usually what you want for an internal admin, and usually not what you want for a public-facing auth screen (see Gotchas).
Compose the footer as a chain in the panel provider, not in config. The config file is deliberately thin; almost all behaviour is opt-in fluent calls. Keep the chain in a dedicated private method if the provider gets noisy:
protected function footer(): EasyFooterPlugin
{
return EasyFooterPlugin::make()
->withFooterPosition('footer')
->withSentence(new HtmlString(
'<img src="/img/logo.svg" width="18" height="18" style="margin-right:.4rem"> Acme'
))
->withLogo('/img/acme-mark.svg', 'https://acme.test', 'Powered by Acme', 24)
->withLinks([...])
->withBorder()
->hiddenFromPagesEnabled();
}
Pick the position by intent, not aesthetics:
footer (default) → global app footer under the content area. Best for legal links and version stamps.sidebar (panels::sidebar.nav.end) → footer glued under the nav items; good for a compact "build info" chip on desktop.sidebar.footer (panels::sidebar.footer) → pinned to the bottom of the sidebar, visually separated. Best for "v2.3.1 · up to date" style badges.Remember these map to render hooks, so on mobile (collapsed sidebar) the sidebar variants may be invisible. If the footer contains compliance-critical links, use footer.
Version stamping is the killer feature for ops. Two independent concepts:
// Zero network calls, reads the Composer-resolved version
->withShowInstalledVersion()
// Adds latest GitHub tag + an "update available" badge (needs github config)
->withGithub(showLogo: true, showUrl: true)
->withShowInstalledVersion(showLatest: true, showUpdatable: true)
showLatest / showUpdatable are silently inert without ->withGithub() — chain both or neither. Configure the repository once in the config file:
'github' => [
'repository' => 'acme/admin',
'token' => env('GITHUB_TOKEN'), // required for private repos, recommended otherwise
'cache_ttl' => 3600,
],
Hide auth pages as a default, then add exceptions. hiddenFromPagesEnabled() alone removes the footer from admin/login, admin/forgot-password, admin/register. Note that passing an explicit array replaces that default set entirely:
->hiddenFromPagesEnabled()
->hiddenFromPages(['settings', 'admin/login', 'admin/register']) // forgot-password now shows it
Load time is a novelty, use it in staging. ->withLoadTime('This page loaded in') reads well on a dev/staging panel and looks amateur on a customer-facing admin. Gate it:
->withLoadTime(app()->isLocal() ? 'Local render time' : null)
(or conditionally register the whole plugin in local/staging only.)
Multi-panel apps: the plugin is per-panel. If you run AdminPanel and AppPanel, register it in each and vary the chain — e.g. full legal footer on the customer panel, version badge on the admin panel, hiddenFromPagesEnabled() only on the public one.
Customising markup: publish views only when the fluent API runs out (logo/sentence/links cover ~90% of needs). Published views live in resources/views/vendor/filament-easy-footer/ and become yours to maintain across package upgrades — treat it as a fork.
The custom-theme requirement is not optional on Filament v4/v5. Tailwind 4 won't scan the vendor views, so footer classes get purged and the footer renders unstyled or invisible. Add to your theme.css before npm run build:
@source '../../../../vendor/devonab/filament-easy-footer/resources/views/**/*';
The number of ../ depends on where your theme CSS sits (resources/css/filament/admin/theme.css needs more levels than resources/css/theme.css). If the footer looks broken, this is the first thing to check — not the plugin config.
withSentence() overrides app_name. Precedence is roughly: withSentence() → github/versioning config → app_name config → config('app.name'). Setting a sentence and then wondering why the app name vanished is the most common confusion.
HtmlString output is filtered to a whitelist: <strong> <img> <a> <em> <span> <b> <i> <small>. Anything else is stripped. Inline style attributes survive (the README's own examples use them), but don't plan on <svg> or Tailwind classes — use the whitelist tags with inline styles.
Links are capped at three. The withLinks() array is documented as 3 max; a fourth entry may render or may not, depending on version. Keep it at three and put the rest in a custom view if you truly need more.
GitHub API calls fail quietly and look like "no version". Symptoms: badge missing, showLatest blank. Causes: wrong repository format (must be owner/name, no URL), missing token on a private repo, token without Contents: read-only permission, or a stale cache_ttl after you push a new tag. Debug by dropping cache_ttl to 60 locally, and remember the value is cached — clearing config/application cache alone won't refresh it.
Installed-version resolution depends on the deploy artifact. If Composer can't resolve the package version (common with VCS-less deploys or archive builds), the badge disappears. Set the fallback:
'versioning' => ['local_fallback_config_key' => 'app.version'],
and populate app.version from your CI (APP_VERSION=$(git describe --tags)). This is also the cleaner long-term pattern — deterministic across environments.
hiddenFromPages() matches paths, so mind the prefix. Values are relative to the panel ('settings', not '/admin/settings') and must match what the router reports — no leading slash, no query string. When a page won't hide, Log::info(request()->path()) on that page and copy the exact string.
Sidebar positions and collapsed sidebars. sidebar / sidebar.footer render inside the sidebar DOM; on mobile the sidebar is off-canvas, so the footer disappears. Don't put legally required content there.
footerEnabled() is a runtime toggle, not a kill switch. The footer is enabled by default; ->footerEnabled() with no argument is a no-op. If you conditionally disable, you need the boolean form (->footerEnabled(false)) — easy to misread from the README example, which shows // true by default next to a bare call.
Maturity reality check: 39 stars, 0 dependents, single maintainer. That's fine for a cosmetic plugin — the blast radius of a breaking change is a footer — but budget for it: pin the version (^2.0), avoid publishing views unless necessary (published views are the main upgrade-friction point), and don't build compliance-critical UX on top of it. The art/ screenshots in the README are effectively the visual spec; there's no long-form docs beyond it.
Extension points, in order of cost: fluent API → config file → published views → your own Blade partial in a panel render hook (if you outgrow the package entirely, ->renderHook(PanelsRenderHook::FOOTER, fn () => view('partials.footer')) replaces it in one line).
How can I help you explore Laravel packages today?