spatie/spatie-content-api
Laravel/PHP package powering Spatie’s promotional-site content API. Fetch posts for a product or project (e.g., mailcoach) via a simple ContentApi facade and expose consistent, reusable content to your frontend or other services.
Installation:
composer require spatie/content-api
Publish the config file:
php artisan vendor:publish --provider="Spatie\ContentApi\ContentApiServiceProvider" --tag="content-api-config"
Define a Content Type:
Create a migration for your content type (e.g., Product):
php artisan make:migration create_products_table
Add fields like title, description, slug, and published_at (timestamps). Run:
php artisan migrate
Register the Content Type:
In config/content-api.php, add your content type under content_types:
'products' => [
'model' => \App\Models\Product::class,
'fields' => [
'title', 'description', 'slug', 'published_at',
],
'sortable_fields' => ['title', 'published_at'],
'filterable_fields' => ['title', 'published_at'],
],
First API Request: Fetch all products via:
GET /api/products
Or filter/sort:
GET /api/products?filter[title]=Laptop&sort=-published_at
$product = \Spatie\ContentApi\Facades\ContentApi::get('products', 'laptop-pro');
Create/Update Content:
Use Laravel’s built-in HTTP methods or a frontend form to POST/PUT to /api/products.
Example payload:
{
"title": "Premium Laptop",
"description": "High-performance laptop...",
"slug": "premium-laptop",
"published_at": "2025-10-15T00:00:00"
}
Validate requests with Laravel’s validation or custom rules.
Soft Deletes:
Enable soft deletes in your model (use \Illuminate\Database\Eloquent\SoftDeletes;).
The API will automatically exclude soft-deleted records unless with_trashed is passed as a query parameter.
Frontend Consumption:
Fetch data in JavaScript (e.g., React/Vue) using axios or fetch:
axios.get('/api/products?filter[slug]=laptop-pro')
.then(response => console.log(response.data));
Cache responses with Laravel’s cache middleware or a CDN.
GraphQL Layer:
Wrap the API in a GraphQL schema (e.g., using spatie/laravel-graphql) for flexible queries:
query {
products(filter: { slug: "laptop-pro" }) {
title
description
}
}
Multi-Language Support:
Extend the fields array in config/content-api.php to include localized fields:
'products' => [
'fields' => [
'title_en', 'title_nl', 'description_en', 'description_nl',
],
'localizable_fields' => ['title', 'description'],
],
Use a package like spatie/laravel-translatable for seamless localization.
Route-Based Localization:
Prefix routes with locale (e.g., /en/api/products) using middleware like spatie/laravel-localization.
spatie/laravel-medialibrary to attach images/videos to content models.
Add a media field to your content type config:
'products' => [
'fields' => ['title', 'description', 'media'],
],
The API will automatically serialize media URLs.Events:
Listen to Spatie\ContentApi\Events\ContentSaved or ContentDeleted to trigger side effects (e.g., notifications, analytics):
\Event::listen(\Spatie\ContentApi\Events\ContentSaved::class, function ($event) {
Log::info("Product saved: {$event->content->title}");
});
Policies: Apply Laravel’s authorization to restrict API access:
class ProductPolicy {
public function update(User $user, Product $product) {
return $user->isAdmin();
}
}
Scopes: Add custom query scopes to your model for advanced filtering:
public function scopeFeatured($query) {
return $query->where('is_featured', true);
}
Register the scope in config/content-api.php:
'products' => [
'scopes' => ['featured'],
],
Pagination:
Use Laravel’s built-in pagination or customize via config/content-api.php:
'pagination' => [
'default_per_page' => 20,
],
Access paginated results in your API responses.
Caching: Cache API responses globally or per-content-type:
\Spatie\ContentApi\Facades\ContentApi::cacheFor(60); // Cache for 60 seconds
config/content-api.php when adding/removing fields in your migration.php artisan content-api:generate-config to auto-generate the config based on your model’s fillable fields.\Spatie\ContentApi\Facades\ContentApi::get('products', null, ['with_trashed' => false]);
Or set with_trashed to false in config/content-api.php as the default.fillable is not strictly defined.fillable in your model and validate incoming requests:
protected $fillable = ['title', 'description', 'slug'];
Use Laravel’s AuthorizesRequests trait to gate sensitive fields.php artisan route:cache) may break dynamic API routes.dd() to inspect raw responses:
Route::get('/debug-products', function () {
return \Spatie\ContentApi\Facades\ContentApi::get('products');
});
DB_LOG_QUERIES=true
\Event::listen(\Spatie\ContentApi\Events\ContentSaved::class, function ($event) {
\Log::channel('single')->info('Content saved', ['content' => $event->content]);
});
ContentApiServiceProvider registers middleware correctly. Add custom middleware to the $middleware array in the provider if needed.public function test_get_product_by_slug() {
$product = Product::factory()->create(['slug' => 'test-slug']);
$response = $this->getJson('/api/products/test-slug');
$response->assertOk()->assertJson(['title' => $product->title]);
}
tests/Feature/ContentApiTest.php.ContentApi for project-specific logic:
class CustomContentApi {
public function getFeaturedProducts() {
return \Spatie\ContentApi\Facades\ContentApi::get('products', null, [
'scopes' => ['featured'],
'limit'
How can I help you explore Laravel packages today?