acidjazz/metapi
Metapi is a Laravel package for managing application metadata through a simple API. Define and organize meta information—such as page details, SEO tags, or custom model metadata—and expose it cleanly in your views, controllers, or JSON responses.
composer require fumeapp/metapi. As of v2.4.0 the package supports Laravel 12.x, so on a fresh L12 app you can pull it in without any constraint gymnastics (PHP 8.2+ is required by Laravel 12 itself).php artisan vendor:publish --tag=metapi (config + migration). Run php artisan migrate before touching any model — the trait will fail silently or throw "no such table" errors otherwise.config/metapi.php. The config is where table names, morph aliases, and any caching behaviour are defined; read it before you write your first saveMeta() call.Page, Post, Product) and attach SEO/Open Graph data to it, then render it in Blade for public pages or expose it through an API resource for a headless frontend.// app/Models/Page.php
use FumeApp\Metapi\Traits\HasMeta; // verify namespace against your version's README
class Page extends Model
{
use HasMeta;
}
$page->saveMeta([
'title' => 'Spring Sale',
'description' => 'Up to 40% off everything.',
'og_image' => '/img/sale.jpg',
]);
echo $page->meta('title'); // or $page->meta->title, depending on version
Trait-per-model, not per-controller. Attach the trait only to models that actually need metadata (Page, Post, Product). Keeping it off User and other high-churn tables keeps the extra relation and joins out of unrelated queries.
Eager load everything you list. Metadata is a relation (typically a morph relation). Any index listing that shows meta titles needs Page::with('meta')->paginate(), otherwise you get an N+1 per row — very easy to miss because the page still renders fine in development.
Write through a form request, not raw arrays. Validate the meta payload in a MetaRequest (max lengths on title/description, URL validation on images) and pass $request->validated('meta') to the save method. This keeps the JSON/column payload predictable and avoids storing null keys that break later ?? fallbacks.
Fallbacks in an accessor. Rather than sprinkling ?? $page->title through Blade, add a small accessor or a MetaPresenter that resolves meta.title ?: $page->name and a default OG image from config. One place to change when marketing asks for new defaults.
Expose via a dedicated API resource. Wrap meta in its own MetaResource and merge it conditionally ($this->mergeWhen($this->meta, ...)) so endpoints that haven't been backfilled don't emit meta: null.
// app/Http/Resources/PageResource.php
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'meta' => new MetaResource($this->whenLoaded('meta')),
];
}
Pair with a sitemap/og Blade partial. Render the same meta record through <x-meta-tags :model="$page" /> for server-rendered pages, and through the resource for the API — one source of truth for both surfaces.
Seed meta in factories for tests. A HasMeta factory state (->hasMeta(['title' => 'Test'])) makes feature tests for the meta endpoint deterministic and avoids depending on migration fixtures.
Treat meta as content, not schema. If you need per-locale metadata, store it keyed by locale inside the meta payload and resolve in the accessor, rather than adding a locale column you later regret.
@laravel-shift). There are no new features, no deprecations, and no breaking changes — upgrading from v2.3.0 is a composer update fumeapp/metapi away. Confirm the constraint is "fumeapp/metapi": "^2.4" before running your L12 upgrade so Composer doesn't resolve the older, L12-incompatible release.php artisan config:clear && php artisan cache:clear after upgrading to Laravel 12; a stale cached config from L11 pointing at an old package provider is the most common "it installed but nothing works" symptom.vendor:publish → migrate → attach trait.cascadeOnDelete or the package registers a cleanup. Verify this before deleting content in production; orphan rows accumulate quietly.$model->update($request->all()), remember that metadata is a separate save path — it won't be filled by fillable on the parent model.$page->id . $page->updated_at or bust the cache in an observer, otherwise edited SEO titles stay stale.toArray()/toJson() on the meta object directly. Dumping the parent model buries the relation; dumping $page->meta (or the result of the getter) shows exactly what was persisted versus what the accessor returns — the usual source of "my title isn't showing" confusion.updated_at-based cache key. Prefer that over subclassing the trait.How can I help you explore Laravel packages today?