| Install | |
|---|---|
composer require idoneo/cms-core |
|
| Latest Version: | v2.0.0 |
| PHP: | ^8.2 |
Multi-tenant CMS foundation with Teams support, Livewire components and Filament integration for Laravel.
To create a new Laravel project with CMS-Core from scratch:
composer create-project laravel/laravel my-project
cd my-project
composer require idoneo/cms-core
php artisan cms-core:install --fresh --seed
npm install && npm run build
This will:
profile.show, teams.show, teams.create, etc.)If you already have a Laravel project, install the package:
composer require idoneo/cms-core
php artisan cms-core:install --fresh --seed
npm install && npm run build
Note: The --fresh flag will drop all existing tables. If you want to keep your data, use:
php artisan cms-core:install --seed
php artisan migrate
To update CMS-Core resources (migrations, config, views, translations) in an existing project:
composer update idoneo/cms-core
php artisan cms-core:update
php artisan migrate
npm run build
Options:
--migrations: Only publish migrations--force: Force publish even if files existIf you need more control:
# Install dependencies
composer require idoneo/cms-core
# Install Jetstream manually
php artisan jetstream:install livewire --teams
# Publish CMS-Core assets
php artisan vendor:publish --tag="cms-core"
# Run migrations
php artisan migrate
# Build assets
npm install && npm run build
Register the plugin in your AdminPanelProvider:
use Idoneo\CmsCore\Filament\CmsCorePlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(CmsCorePlugin::make())
This package includes Spatie Laravel Tags for tagging and categorization. See docs/TAGS.md for complete documentation.
Quick example:
use Spatie\Tags\HasTags;
use Filament\Forms\Components\SpatieTagsInput;
class Post extends Model
{
use HasTags;
}
// In Filament Resource
SpatieTagsInput::make('tags')
->label('Tags')
// For categorization
SpatieTagsInput::make('categories')
->type('categories')
// ... other configuration
}
After running with --seed flag:
| Field | Value |
|---|---|
hola@humano.app |
|
| Password | Simplicity! |
Add to your .env file:
# Show/hide teams UI features (default: false)
# Note: Users always have a personal team (Jetstream requirement)
# This setting only controls UI visibility (team switcher, create team, team settings)
APP_TEAMS=false
When APP_TEAMS=false:
When APP_TEAMS=true:
CMS-Core automatically configures brand logos for your Filament panel if you place logo files in public/custom/:
Supported files:
logo-light.svg - Logo for light modelogo-dark.svg - Logo for dark modeSetup:
public/custom/ directory (if it doesn't exist)public/
└── custom/
├── logo-light.svg
└── logo-dark.svg
Note: Only SVG files are supported. The plugin automatically detects and configures the logos without any additional configuration needed.
Add to models that should be scoped by team:
use Idoneo\CmsCore\Traits\BelongsToCurrentTeam;
class Project extends Model
{
use BelongsToCurrentTeam;
}
This automatically:
current_team_idteam_id on createforTeam() and withoutTeamScope() methodsInclude in your Blade views:
@if(\Idoneo\CmsCore\CmsCore::teamsEnabled())
<livewire:cms-core::team-switcher />
@endif
Register in your Panel Provider:
use Idoneo\CmsCore\Filament\CmsCorePlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugins([
CmsCorePlugin::make(),
]);
}
use Idoneo\CmsCore\CmsCore;
// Check if teams are enabled
CmsCore::teamsEnabled();
// Get configured models
CmsCore::teamModel();
CmsCore::userModel();
After installation, configure team roles in app/Providers/JetstreamServiceProvider.php:
protected function configurePermissions(): void
{
Jetstream::defaultApiTokenPermissions(['read']);
Jetstream::role('admin', 'Administrator', [
'create',
'read',
'update',
'delete',
])->description('Administrator users can perform any action.');
Jetstream::role('member', 'Member', [
'read',
'create',
'update',
])->description('Members have standard access to create and manage content.');
Jetstream::role('viewer', 'Viewer', [
'read',
])->description('Viewers can only read and view content.');
}
These are suggested roles that work for most use cases. You can customize them:
By default, all authenticated users can access the Filament panel. To restrict access, implement FilamentUser in your User model:
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
class User extends Authenticatable implements FilamentUser
{
public function canAccessPanel(Panel $panel): bool
{
// Allow all authenticated users (default behavior)
return true;
// Or restrict by email domain
// return str_ends_with($this->email, '@yourdomain.com');
// Or use roles with Spatie Permission
// return $this->hasRole('admin');
}
}
CMS-Core provides a RESTful API for accessing posts with authentication via Sanctum tokens.
All API endpoints require authentication using a Bearer token. You can generate a token in two ways:
Generate a token and add it to your .env file:
# Generate token for admin user (default)
php artisan cms-core:api-token
# Or specify a user email
php artisan cms-core:api-token --email=hola@humano.app --name="API Token"
Output example:
API Token generated successfully!
Token Name: API Token
User: hola@humano.app
Add this to your .env file:
APP_TOKEN=1|abc123def456ghi789jkl012mno345pqr678stu901vwx234yz
⚠️ Save this token now! You won't be able to see it again.
Copy the token and add it to your .env file:
APP_TOKEN=1|abc123def456ghi789jkl012mno345pqr678stu901vwx234yz
Then use it in your curl requests:
# Replace YOUR_TOKEN with the token from .env
curl -X GET "http://localhost:8000/api/posts" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
Generate a token in your user profile settings at /user/api-tokens or via Jetstream's API token management interface.
Headers:
Authorization: Bearer {your-token}
Content-Type: application/json
Accept: application/json
If you have APP_TEAMS=true in your .env but the teams options don't appear in the user menu, run the diagnostic command:
php artisan cms-core:diagnose
This will check:
Common solutions:
# 1. Clear config cache
php artisan config:clear
# 2. Verify .env has APP_TEAMS=true (no spaces)
echo "APP_TEAMS=true" >> .env
# 3. Republish config
php artisan vendor:publish --tag=cms-core-config --force
# 4. Clear cache again
php artisan config:clear
# 5. Restart server (if using php artisan serve)
Accept: application/json
**Note:** If you set `APP_TOKEN` in your `.env`, you can use that token directly. The system will automatically authenticate the user associated with that token.
#### Quick Start Example
1. **Generate a token:**
```bash
php artisan api:token --email=hola@humano.app
.env file:APP_TOKEN=1|abc123def456ghi789...
# List all published posts
curl -X GET "http://localhost:8000/api/posts" \
-H "Authorization: Bearer 1|abc123def456ghi789..." \
-H "Accept: application/json"
# Or use the token from .env (if you have it exported)
curl -X GET "http://localhost:8000/api/posts" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Accept: application/json"
Get a paginated list of posts with optional filters.
GET /api/posts
Query Parameters:
status (optional): Filter by status (draft, published, archived). Default: publishedcategory (optional): Filter by category nametag (optional): Filter by tag namesearch (optional): Search in title, excerpt, and contentper_page (optional): Number of items per page (1-100, default: 15)page (optional): Page number (default: 1)Example Request:
# Using token from .env (APP_TOKEN)
curl -X GET "http://localhost:8000/api/posts?status=published&category=Tutorials&per_page=10" \
-H "Authorization: Bearer $(php artisan tinker --execute='echo env(\"APP_TOKEN\");')" \
-H "Accept: application/json"
# Or using a token directly
curl -X GET "http://localhost:8000/api/posts?status=published&category=Tutorials&per_page=10" \
-H "Authorization: Bearer 1|your-token-here" \
-H "Accept: application/json"
# Simple example (replace YOUR_TOKEN with your actual token)
curl -X GET "http://localhost:8000/api/posts" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
Example Response:
{
"data": [
{
"id": 1,
"title": "My First Post",
"slug": "my-first-post",
"excerpt": "This is an excerpt...",
"content": "Full content here...",
"status": "published",
"published_at": "2024-01-15T10:00:00Z",
"created_at": "2024-01-15T09:00:00Z",
"updated_at": "2024-01-15T09:30:00Z",
"author": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"featured_image": {
"url": "https://...",
"thumb": "https://...",
"web": "https://..."
},
"gallery": [
{
"url": "https://...",
"thumb": "https://...",
"web": "https://..."
}
],
"categories": [
{
"id": 1,
"name": "Tutorials",
"slug": "tutorials"
}
],
"tags": [
{
"id": 2,
"name": "Laravel",
"slug": "laravel"
}
]
}
],
"links": {
"first": "https://...",
"last": "https://...",
"prev": null,
"next": "https://..."
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 10,
"to": 10,
"total": 50
}
}
Get a single post by its slug.
GET /api/posts/{slug}
Example Request:
# Using token from .env (APP_TOKEN)
curl -X GET "http://localhost:8000/api/posts/my-first-post" \
-H "Authorization: Bearer $(php artisan tinker --execute='echo env(\"APP_TOKEN\");')" \
-H "Accept: application/json"
# Or using a token directly
curl -X GET "http://localhost:8000/api/posts/my-first-post" \
-H "Authorization: Bearer 1|your-token-here" \
-H "Accept: application/json"
# Simple example (replace YOUR_TOKEN with your actual token)
curl -X GET "http://localhost:8000/api/posts/my-first-post" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
Example Response:
{
"data": {
"id": 1,
"title": "My First Post",
"slug": "my-first-post",
"excerpt": "This is an excerpt...",
"content": "Full content here...",
"status": "published",
"published_at": "2024-01-15T10:00:00Z",
"created_at": "2024-01-15T09:00:00Z",
"updated_at": "2024-01-15T09:30:00Z",
"author": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"featured_image": {
"url": "https://...",
"thumb": "https://...",
"web": "https://..."
},
"gallery": [],
"categories": [],
"tags": []
}
}
If teams are enabled (APP_TEAMS=true), the API automatically filters posts by the authenticated user's current team. Users can only access posts from their current team.
401 Unauthorized:
{
"message": "Unauthenticated."
}
404 Not Found:
{
"message": "No query results for model [App\\Models\\Post] {slug}"
}
422 Validation Error:
{
"message": "The given data was invalid.",
"errors": {
"status": ["The selected status is invalid."]
}
}
Here are ready-to-use curl examples (replace YOUR_TOKEN with your actual token):
List all published posts:
curl -X GET "http://localhost:8000/api/posts" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
List posts with filters:
curl -X GET "http://localhost:8000/api/posts?status=published&category=Tutorials&search=laravel&per_page=5" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
Get a specific post by slug:
curl -X GET "http://localhost:8000/api/posts/my-first-post" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
Filter by tag:
curl -X GET "http://localhost:8000/api/posts?tag=laravel" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"
Pretty print JSON response (using jq):
curl -X GET "http://localhost:8000/api/posts" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json" | jq
If you discover a security vulnerability, please send an e-mail to Diego Mascarenhas Goytía via diego.mascarenhas@icloud.com. All security vulnerabilities will be promptly addressed.
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
By deploying this software, you agree to notify the original author at diego.mascarenhas@icloud.com or by visiting linkedin.com/in/diego-mascarenhas. Any modifications or enhancements must be shared with the original author.