Feedable models
Anything that appears in the feed — actor, object, target, or context — implements Feedable:
use Illuminate\Database\Eloquent\Model;
use Storyfeed\Concerns\InteractsWithFeed;
use Storyfeed\Contracts\Feedable;
use Storyfeed\FeedEntity;
use Storyfeed\FeedLink;
class Document extends Model implements Feedable
{
use InteractsWithFeed;
public function toFeed(): FeedEntity
{
return FeedEntity::make(
label: $this->name,
data: ['id' => $this->id, 'project_id' => $this->project_id],
);
}
public static function toFeedLink(array $data): ?FeedLink
{
return FeedLink::make(url: route('documents.show', $data['id']));
}
}Snapshots and links
The two methods split along the cache boundary:
| method | runs at | produces |
|---|---|---|
toFeed() | publish time (refreshed on save) | the cached snapshot: label + data |
toFeedLink() | read time, statically, from the cached data | a fresh URL |
Reads never touch your domain tables — a feed page is served entirely from snapshots. URLs are regenerated live so they never go stale.
TIP
toFeedLink() receives exactly what toFeed() put in data — include the key you need to build the URL. Throwing inside it is safe: the failure is reported and the entity degrades to url: null. One broken link never breaks a feed.
FeedLink carries more than a URL when you need it:
FeedLink::make(url: $url, attributes: ['target' => '_blank']);
FeedLink::modal($url); // hint the renderer to open as a modalKeeping snapshots fresh
InteractsWithFeed wires the model events: saving refreshes the snapshot, deleting removes the entity's feed presence.
| method | use |
|---|---|
updateFeedSnapshot() | force a refresh outside a save |
deleteFromFeed() | remove feed presence (soft) |
forceDeleteFromFeed() | remove permanently |
For entities recorded before they had snapshots (imports, backfills), schedule the trickle:
Schedule::command('storyfeed:trickle')->everyMinute();Un-snapshotted entities still appear — with label: null, url: null — and renderers show a neutral placeholder. Activities are never hidden by the read path.
Morph aliases
Storyfeed stores morph aliases, never class names, so entities survive a namespace refactor. Enforce a map:
Relation::enforceMorphMap([
'document' => Document::class,
'project' => Project::class,
'user' => User::class,
]);Aliases can also be registered in config/storyfeed.php under morph_map, which merges into the app's map at boot.
Rich rendering
FeedEntity optionally names a frontend component and passes it props:
FeedEntity::make(
label: $this->name,
component: 'Resource',
data: ['status' => $this->status],
);The payload carries component and data on the entity; what your renderer does with them is yours.