awcodes/recently
Recently adds a "Recently Viewed" menu to Filament 4 and 5 panels, tracking visits to EditRecord and ViewRecord pages. Install via Composer, add the plugin, and include the trait on resource pages so users can quickly reopen records they've interacted with.
Fit check first. awcodes/recently is a small, single-purpose Filament plugin: it records visits to a resource's EditRecord/ViewRecord pages and surfaces them as a topbar dropdown menu and as global-search results. It is not a general audit log, not an activity feed, and it does not track ListRecords, dashboard pages, or custom pages. If that matches your need, it is a couple of hours of work. If you need "who changed what", use spatie/laravel-activitylog instead.
Version pinning. The README's compatibility table does not fully agree with the release history (2.x is documented as Filament 5.x, but the 2.0.0 release notes say Filament v4; 3.x is stated as v4 & v5). Don't trust the table — check the composer.json constraint of the tag you install and pin explicitly:
composer require awcodes/recently:^3.0
For Filament 3 you need the 1.x line; 3.x is the current line for Filament 4/5 and also carries the newest features (pruning, deleted-record hiding).
Install and publish.
composer require awcodes/recently
php artisan recently:install
The install command is interactive — it will offer to publish the config and migrations. Say yes to both on a fresh install; it saves you a round trip later.
Custom theme is mandatory on panels. If your panel uses a custom theme (most production panels do), the plugin's Blade views must be scanned by Tailwind or the dropdown renders unstyled:
/* resources/css/filament/admin/theme.css */
@source '../../../../vendor/awcodes/recently/resources/**/*.blade.php';
Adjust the relative depth to your theme file location, then rebuild assets (npm run build). Forgetting this is the single most common "it's broken" report.
Register per panel.
use Awcodes\Recently\RecentlyPlugin;
public function panel(Panel $panel): Panel
{
return $panel->plugins([
RecentlyPlugin::make(),
]);
}
Opt in per page. This is the part people miss: the trait goes on the page class, not the resource.
use Awcodes\Recently\Concerns\HasRecentHistoryRecorder;
class EditUser extends EditRecord
{
use HasRecentHistoryRecorder;
protected static string $resource = UserResource::class;
}
Where to look first:
config/recently.php after publishing — user_model must match your authenticatable model or nothing records.recent_entries table — the fastest way to confirm tracking works.docs.aw.codes/recently/3.x/configuration — the README is a summary; the configuration page documents the 3.2 options (max_stored_items, prune_after_days, track_records, include_trashed_records) that the README body does not.Awcodes\Recently\RecentlyPlugin source — all fluent options are there and it's a small class; read it once and you know the whole surface area.First use case: put the trait on EditUser and ViewUser, visit a user, then look at the topbar and the global search modal. That end-to-end loop takes about ten minutes and validates theme, config, and trait wiring simultaneously.
Be selective about what you track. Tracking every resource floods the menu with noise and inflates the table. Track the two or three resources people actually work in daily (orders, tickets, customers) and skip the long tail of settings resources. Because the trait lives on the page class, selectivity is free — just don't add it.
Configure per panel, not just globally. Everything in config/recently.php has a fluent equivalent, so an admin panel and a lightweight operator panel can differ:
RecentlyPlugin::make()
->maxItems(5)
->label('Recent')
->icon('heroicon-o-clock')
->width('sm')
->menu()
->globalSearch(condition: false);
Use globalSearch(false) in panels where the global search modal is already dense with resource results — recent entries there often push real matches down.
Render hook relocation. The default is PanelsRenderHook::USER_MENU_BEFORE. On panels with a crowded topbar, move it after the user menu or into the sidebar render hooks:
use Filament\View\PanelsRenderHook;
RecentlyPlugin::make()
->renderUsingHook(PanelsRenderHook::USER_MENU_AFTER);
Enable pruning from day one on busy panels. recent_entries grows one row per user per record per visit, forever. In 3.2+:
'max_stored_items' => 50,
'prune_after_days' => 90,
prune_after_days only takes effect if model:prune is scheduled — add it to routes/console.php or the scheduler, otherwise the retention setting is decorative. max_stored_items is the more important of the two: it caps per-user growth regardless of scheduling.
Enable track_records to avoid dead links. Without it, a link to a since-deleted record leads to a 404/403 page, which users report as a bug. With it, entries whose records no longer exist are hidden, and include_trashed_records keeps soft-deleted ones visible. This requires the additional migration:
php artisan vendor:publish --tag="recently-migrations"
php artisan migrate
Conflict exclusion. RecentEntryResource ships with the plugin and will show up in other resource-listing plugins. Add the exclusion to both:
OverlookPlugin::make()->excludes([RecentEntryResource::class]),
QuickCreatePlugin::make()->excludes([RecentEntryResource::class]),
Audit for any other plugin that enumerates resources (navigation managers, resource finders, permission seeders). A RecentEntryResource leaking into your permissions table or sitemap is a subtle, easy-to-miss annoyance.
Testing pattern. Add a feature test that asserts a row is created after visiting a page; it protects you against a Filament upgrade silently breaking the trait:
$this->actingAs($user)
->get(UserResource::getUrl('edit', ['record' => $user]))
->assertOk();
$this->assertDatabaseHas('recent_entries', ['user_id' => $user->id]);
Fallback if the package is abandoned. The core is a belongsTo user model, a table, a trait with a mount-time create/updateOrCreate, and a dropdown Blade view. That's roughly 100 lines to reimplement. Before adopting, note the 43 stars and single maintainer — acceptable for a UI convenience, not something to build your navigation strategy on. Vendoring the plugin into app/ or a local path repository is a cheap insurance policy.
The README's renderUsingHook example is wrong. It shows a quoted string:
->renderUsingHook('PanelsRenderHook::USER_MENU_AFTER') // ❌ string literal
Pass the constant itself, or the hook name string without the class prefix:
->renderUsingHook(PanelsRenderHook::USER_MENU_AFTER) // ✅
max_items ≠ storage limit. max_items only truncates what the dropdown displays. It does nothing to the table size. If you set max_items(10) and assume you're done, you'll be back in six months with a six-figure recent_entries table. Pair it with max_stored_items.
Trait on the resource does nothing. HasRecentHistoryRecorder must be on the concrete page class. If entries aren't appearing, this is cause #1; cause #2 is user_model pointing at the wrong class or a non-standard auth guard.
Multi-panel and multi-tenant ambiguity. Entries are keyed by user, not by panel or tenant. Verify on a staging copy with two panels and two tenants whether an entry viewed in one context appears in the other. If it leaks across tenants, you'll need to scope the plugin's query (fork or a custom entry model via the model config key added in 3.0.2) — the same applies to any record-level authorization: the query scope is user-scoped, but confirm the click-through is still gated by your policies.
SPA mode, Livewire routes, and query strings are handled. Releases 1.0.4 and later ignore Livewire endpoints and query parameters, so table filters and pagination don't create duplicate entries. Don't try to "fix" what looks like deduplication — it's intentional. Note the flip side: two visits to the same record with different tabs/params count as one entry.
Upgrade ordering for 3.2. Publish and run the new migration before setting track_records => true. Enabling the flag against an unmigrated table throws, and existing rows won't have record references, so old entries may behave inconsistently with the new filtering — check the dropdown on a seeded database after enabling rather than trusting a fresh dev environment.
Upgrading from 1.x to 3.x is a major jump (Filament 3 → 4/5). Do the Filament upgrade first, re-register the plugin against the new panel API, then re-verify the theme @source path — the vendor directory depth changes if you relocate packages.
Debug checklist, in order: row in recent_entries? → user_model correct? → trait on the page class? → theme rebuilt after adding @source? → RecentEntryResource excluded from conflicting plugins? → scheduler running model:prune? Working through that list resolves nearly every issue with this package.
How can I help you explore Laravel packages today?