CMS Framework - v1.0.0
Comprehensive Development Guide
Building Production-Ready Content Management Systems with ArtisanPack UI CMS Framework
Table of Contents
- Introduction
- Getting Started
- Core Architecture
- Step-by-Step CMS Creation
- Advanced Features
- Recommended Package Ecosystem
- Best Practices
- Performance & Security
- Deployment
- Troubleshooting
Introduction
The ArtisanPack UI CMS Framework is a modern, modular Laravel package designed for building sophisticated content management systems. This guide provides a complete walkthrough for developers to create production-ready CMS applications from start to finish.
Why Choose ArtisanPack UI CMS Framework?
- Modern Architecture: Built on Laravel 12+ and PHP 8.2+
- Frontend Agnostic: Works with any frontend framework (React, Vue, Angular, etc.)
- Modular Design: Feature-based architecture with dedicated service providers
- Enterprise Ready: Built-in 2FA, audit logging, and security features
- Developer Experience: Comprehensive testing, documentation, and tooling
- Extensible: Plugin and theme system for customization
What You'll Build
By following this guide, you'll create a full-featured CMS with:
- Content management with custom post types
- User authentication and role-based access
- Media library with file management
- Admin dashboard with widgets
- RESTful API with Sanctum authentication
- Progressive Web App (PWA) capabilities
- Theme and plugin system
Getting Started
Prerequisites
Before starting, ensure you have:
- PHP 8.2 or higher
- Composer 2.0+
- Laravel 12.0+
- Node.js 18+ (for frontend assets)
- MySQL 8.0+, PostgreSQL 13+, or SQLite 3.35+
System Requirements
Minimum Requirements:
- Memory: 256MB PHP memory limit
- Extensions:
ext-json,ext-mbstring,ext-openssl,ext-pdo,ext-tokenizer,ext-xml
Recommended:
- Memory: 512MB+ PHP memory limit
- Redis for caching and sessions
- Elasticsearch for advanced search (optional)
Core Architecture
Framework Structure
The CMS framework follows a modular architecture with clear separation of concerns:
src/
├── CMSManager.php # Main framework manager
├── AuthServiceProvider.php # Authentication provider
├── Contracts/ # Interface definitions
├── Features/ # Feature-based modules
│ ├── AdminPages/ # Admin interface management
│ ├── DashboardWidgets/ # Dashboard widget system
│ ├── Settings/ # Configuration management
│ ├── ContentTypes/ # Content type system
│ ├── Media/ # Media library
│ └── PWA/ # Progressive Web App
└── Traits/ # Reusable traits
Key Components
- Managers: Handle business logic for each feature
- Service Providers: Register services and boot features
- Contracts: Define interfaces for dependency injection
- Models: Eloquent models for data persistence
- Policies: Authorization rules for access control
Step-by-Step CMS Creation
Phase 1: Project Setup and Installation
1.1 Create a New Laravel Project
# Create new Laravel project
laravel new my-cms
cd my-cms
# Or using Composer
composer create-project laravel/laravel my-cms
cd my-cms
1.2 Install the CMS Framework
# Install the CMS framework
composer require artisanpack-ui/cms-framework
# Publish configuration files
php artisan vendor:publish --tag=cms-config
# Run migrations
php artisan migrate
# Create admin user
php artisan cms:user:create --admin
1.3 Configure Environment
Update your .env file with CMS-specific configuration:
# CMS Framework Configuration
CMS_ENABLED=true
CMS_DEFAULT_ROLE=editor
CMS_ALLOW_REGISTRATION=true
# Database Configuration
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=my_cms
DB_USERNAME=root
DB_PASSWORD=
# Media Configuration
CMS_MEDIA_DISK=public
CMS_MEDIA_MAX_SIZE=10240
CMS_MEDIA_ALLOWED_TYPES=jpg,jpeg,png,gif,pdf,doc,docx
# Two-Factor Authentication
CMS_2FA_ENABLED=true
CMS_2FA_ISSUER="My CMS"
# PWA Configuration
CMS_PWA_ENABLED=true
CMS_PWA_NAME="My CMS"
CMS_PWA_SHORT_NAME="CMS"
1.4 Install Sanctum for API Authentication
php artisan sanctum:install
php artisan migrate
Phase 2: Basic CMS Structure
2.1 Create Custom Admin Routes
Create routes/cms_admin.php:
<?php
use ArtisanPackUI\CMSFramework\Features\AdminPages\AdminPagesManager;
/*
|--------------------------------------------------------------------------
| CMS Admin Routes
|--------------------------------------------------------------------------
*/
// Main Dashboard
app(AdminPagesManager::class)->registerPage(
'Dashboard',
'dashboard',
'home',
component: \App\Http\Livewire\Admin\Dashboard\MainDashboard::class
);
// Content Management
app(AdminPagesManager::class)->registerPage(
'Content',
'content',
'document',
view: 'admin.content.index'
);
app(AdminPagesManager::class)->registerSubPage(
'content',
'Posts',
'posts',
component: \App\Http\Livewire\Admin\Content\Posts\Index::class
);
app(AdminPagesManager::class)->registerSubPage(
'content',
'Pages',
'pages',
component: \App\Http\Livewire\Admin\Content\Pages\Index::class
);
// Settings
app(AdminPagesManager::class)->registerPage(
'Settings',
'settings',
'cog',
component: \App\Http\Livewire\Admin\Settings\GeneralSettings::class
);
2.2 Register Admin Routes
Update app/Providers/RouteServiceProvider.php:
public function boot(): void
{
parent::boot();
// Register CMS admin routes
if (file_exists(base_path('routes/cms_admin.php'))) {
require base_path('routes/cms_admin.php');
}
}
2.3 Create Content Types
Register custom content types in app/Providers/AppServiceProvider.php:
use ArtisanPackUI\CMSFramework\Features\ContentTypes\ContentTypeManager;
public function boot(): void
{
// Register Article content type
app(ContentTypeManager::class)->register('article', [
'name' => 'Article',
'plural' => 'Articles',
'description' => 'Blog articles and posts',
'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'tags'],
'taxonomies' => ['categories', 'tags'],
]);
// Register Page content type
app(ContentTypeManager::class)->register('page', [
'name' => 'Page',
'plural' => 'Pages',
'description' => 'Static pages',
'supports' => ['title', 'editor', 'thumbnail', 'template'],
'hierarchical' => true,
]);
// Register Product content type
app(ContentTypeManager::class)->register('product', [
'name' => 'Product',
'plural' => 'Products',
'description' => 'E-commerce products',
'supports' => ['title', 'editor', 'thumbnail', 'gallery', 'custom_fields'],
'taxonomies' => ['product_categories'],
'custom_fields' => [
'price' => 'decimal',
'sku' => 'string',
'inventory' => 'integer',
],
]);
}
Phase 3: Creating Admin Components
3.1 Main Dashboard Component
Create app/Http/Livewire/Admin/Dashboard/MainDashboard.php:
<?php
namespace App\Http\Livewire\Admin\Dashboard;
use Livewire\Component;
use ArtisanPackUI\CMSFramework\Features\DashboardWidgets\DashboardWidgetsManager;
class MainDashboard extends Component
{
public function render()
{
$widgets = app(DashboardWidgetsManager::class)->getWidgets('main_dashboard');
return view('admin.dashboard.main', compact('widgets'))
->layout('components.layouts.admin');
}
}
3.2 Dashboard View
Create resources/views/admin/dashboard/main.blade.php:
<div class="space-y-6">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">Dashboard</h1>
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
Overview of your content management system.
</p>
</div>
</div>
{{-- Dashboard Widgets --}}
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
@foreach($widgets as $widget)
{!! $widget->render() !!}
@endforeach
</div>
{{-- Quick Actions --}}
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">
Quick Actions
</h3>
<div class="mt-6 flow-root">
<div class="flex space-x-4">
<a href="/admin/content/posts/create"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700">
<x-artisanpack-icon name="plus" class="w-4 h-4 mr-2" />
New Post
</a>
<a href="/admin/content/pages/create"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
<x-artisanpack-icon name="document" class="w-4 h-4 mr-2" />
New Page
</a>
</div>
</div>
</div>
</div>
</div>
3.3 Custom Dashboard Widgets
Create app/Http/Livewire/Admin/Widgets/ContentStatsWidget.php:
<?php
namespace App\Http\Livewire\Admin\Widgets;
use ArtisanPackUI\CMSFramework\Features\DashboardWidgets\Widgets\DashboardWidget;
class ContentStatsWidget extends DashboardWidget
{
public string $title = 'Content Statistics';
public string $description = 'Overview of content in your CMS';
public int $priority = 10;
public function render(): string
{
$stats = [
'posts' => \App\Models\Post::count(),
'pages' => \App\Models\Page::count(),
'published' => \App\Models\Post::where('status', 'published')->count(),
'drafts' => \App\Models\Post::where('status', 'draft')->count(),
];
return view('admin.widgets.content-stats', compact('stats'))->render();
}
}
Phase 4: Content Management
4.1 Content Models
Create content models using Artisan commands:
# Create Post model
php artisan make:model Post -mfc
# Create Page model
php artisan make:model Page -mfc
# Create Category model
php artisan make:model Category -mfc
# Create Tag model
php artisan make:model Tag -mfc
4.2 Post Model Example
Update app/Models/Post.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use ArtisanPackUI\CMSFramework\Traits\HasContent;
class Post extends Model
{
use HasFactory, HasContent;
protected $fillable = [
'title',
'slug',
'content',
'excerpt',
'status',
'published_at',
'featured_image',
'meta_title',
'meta_description',
'author_id',
];
protected $casts = [
'published_at' => 'datetime',
'meta_data' => 'array',
];
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class)->withTimestamps();
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
public function scopePublished($query)
{
return $query->where('status', 'published')
->where('published_at', '<=', now());
}
}
Advanced Features
Theme System
Creating Custom Themes
The CMS framework includes a powerful theme system for customizing appearance and functionality.
Create a theme directory:
mkdir -p resources/themes/my-theme
mkdir -p resources/themes/my-theme/{views,assets,config}
Theme Configuration
Create resources/themes/my-theme/theme.json:
{
"name": "My Custom Theme",
"description": "A beautiful theme for my CMS",
"version": "1.0.0",
"author": "Your Name",
"screenshot": "screenshot.png",
"supports": [
"custom-header",
"custom-background",
"menus",
"widgets"
],
"template_parts": {
"header": "partials/header",
"footer": "partials/footer",
"sidebar": "partials/sidebar"
}
}
Theme Service Provider
Create app/Providers/ThemeServiceProvider.php:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use ArtisanPackUI\CMSFramework\Features\Themes\ThemeManager;
class ThemeServiceProvider extends ServiceProvider
{
public function boot(): void
{
$themeManager = app(ThemeManager::class);
// Register theme
$themeManager->register('my-theme', [
'name' => 'My Custom Theme',
'path' => resource_path('themes/my-theme'),
'views' => resource_path('themes/my-theme/views'),
'assets' => resource_path('themes/my-theme/assets'),
]);
// Set active theme
$themeManager->activate('my-theme');
}
}
Plugin System
Creating Plugins
Create a plugin structure:
mkdir -p app/Plugins/ContactForm/{Controllers,Views,Routes}
Plugin Class
Create app/Plugins/ContactForm/ContactFormPlugin.php:
<?php
namespace App\Plugins\ContactForm;
use ArtisanPackUI\CMSFramework\Features\Plugins\Plugin;
class ContactFormPlugin extends Plugin
{
protected string $name = 'Contact Form';
protected string $description = 'Simple contact form plugin';
protected string $version = '1.0.0';
public function boot(): void
{
$this->registerRoutes();
$this->registerViews();
$this->registerShortcodes();
}
protected function registerRoutes(): void
{
require __DIR__ . '/Routes/web.php';
}
protected function registerViews(): void
{
$this->app['view']->addNamespace('contact-form', __DIR__ . '/Views');
}
protected function registerShortcodes(): void
{
add_shortcode('contact_form', [$this, 'renderContactForm']);
}
public function renderContactForm($attributes = []): string
{
return view('contact-form::form', compact('attributes'))->render();
}
}
API Development
API Controllers
Create API controllers for your content:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\Request;
use App\Http\Resources\PostResource;
class PostController extends Controller
{
public function index(Request $request)
{
$posts = Post::published()
->with(['author', 'categories', 'tags'])
->when($request->search, function ($query, $search) {
$query->where('title', 'like', "%{$search}%")
->orWhere('content', 'like', "%{$search}%");
})
->when($request->category, function ($query, $category) {
$query->whereHas('categories', function ($q) use ($category) {
$q->where('slug', $category);
});
})
->orderBy('published_at', 'desc')
->paginate(10);
return PostResource::collection($posts);
}
public function show(string $slug)
{
$post = Post::published()
->with(['author', 'categories', 'tags'])
->where('slug', $slug)
->firstOrFail();
return new PostResource($post);
}
}
API Resources
Create app/Http/Resources/PostResource.php:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'content' => $this->content,
'excerpt' => $this->excerpt,
'status' => $this->status,
'published_at' => $this->published_at?->toISOString(),
'featured_image' => $this->featured_image,
'meta' => [
'title' => $this->meta_title,
'description' => $this->meta_description,
],
'author' => new UserResource($this->whenLoaded('author')),
'categories' => CategoryResource::collection($this->whenLoaded('categories')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
];
}
}
API Routes
Create routes/api.php:
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\{PostController, PageController, CategoryController};
// Public API routes
Route::prefix('v1')->group(function () {
// Posts
Route::get('posts', [PostController::class, 'index']);
Route::get('posts/{slug}', [PostController::class, 'show']);
// Pages
Route::get('pages', [PageController::class, 'index']);
Route::get('pages/{slug}', [PageController::class, 'show']);
// Categories
Route::get('categories', [CategoryController::class, 'index']);
Route::get('categories/{slug}/posts', [CategoryController::class, 'posts']);
});
// Protected API routes (require Sanctum authentication)
Route::middleware('auth:sanctum')->prefix('v1')->group(function () {
// Content management
Route::apiResource('posts', PostController::class)->except(['index', 'show']);
Route::apiResource('pages', PageController::class)->except(['index', 'show']);
Route::apiResource('categories', CategoryController::class);
// User management
Route::get('user', function (Request $request) {
return $request->user();
});
});
Progressive Web App (PWA) Integration
Enable PWA Features
The CMS framework includes built-in PWA support. Configure in your .env:
CMS_PWA_ENABLED=true
CMS_PWA_NAME="My CMS"
CMS_PWA_SHORT_NAME="CMS"
CMS_PWA_THEME_COLOR="#4F46E5"
CMS_PWA_BACKGROUND_COLOR="#FFFFFF"
PWA Manifest
The framework automatically generates a manifest.json file, but you can customize it:
// In a service provider
use ArtisanPackUI\CMSFramework\Features\PWA\PWAManager;
app(PWAManager::class)->configure([
'name' => config('app.name'),
'short_name' => config('cms.pwa.short_name'),
'description' => 'A powerful content management system',
'icons' => [
[
'src' => '/images/icon-192x192.png',
'sizes' => '192x192',
'type' => 'image/png',
],
[
'src' => '/images/icon-512x512.png',
'sizes' => '512x512',
'type' => 'image/png',
],
],
'start_url' => '/',
'display' => 'standalone',
'theme_color' => '#4F46E5',
'background_color' => '#FFFFFF',
]);
Service Worker
Create public/sw.js for offline functionality:
const CACHE_NAME = 'cms-v1';
const urlsToCache = [
'/',
'/css/app.css',
'/js/app.js',
'/images/logo.png',
];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
Two-Factor Authentication
Enable 2FA
The framework includes built-in 2FA support:
// In your User model
use ArtisanPackUI\CMSFramework\Traits\TwoFactorAuthenticatable;
class User extends Authenticatable
{
use TwoFactorAuthenticatable;
// ... rest of your model
}
2FA Setup Component
Create a Livewire component for 2FA setup:
<?php
namespace App\Http\Livewire\Profile;
use Livewire\Component;
use Illuminate\Support\Collection;
class TwoFactorAuthenticationForm extends Component
{
public bool $showingQrCode = false;
public bool $showingRecoveryCodes = false;
public Collection $recoveryCodes;
public function enableTwoFactorAuthentication()
{
$this->user->enableTwoFactorAuthentication();
$this->showingQrCode = true;
$this->showingRecoveryCodes = true;
$this->recoveryCodes = collect($this->user->recoveryCodes());
}
public function disableTwoFactorAuthentication()
{
$this->user->disableTwoFactorAuthentication();
$this->reset();
}
public function regenerateRecoveryCodes()
{
$this->user->regenerateRecoveryCodes();
$this->recoveryCodes = collect($this->user->recoveryCodes());
$this->showingRecoveryCodes = true;
}
public function getUserProperty()
{
return auth()->user();
}
public function render()
{
return view('profile.two-factor-authentication-form');
}
}
Recommended Package Ecosystem
ArtisanPack UI Packages
The ArtisanPack UI ecosystem provides a comprehensive suite of packages designed to work seamlessly together:
1. artisanpack-ui/livewire-ui-components
Essential UI components for building modern interfaces with Livewire.
composer require artisanpack-ui/livewire-ui-components
Key Features:
- Pre-built Livewire components
- Form components with validation
- Data tables and pagination
- Modal dialogs and notifications
- Navigation components
Usage Example:
<x-artisanpack-form wire:submit="save">
<x-artisanpack-input
label="Title"
wire:model="title"
required
/>
<x-artisanpack-textarea
label="Content"
wire:model="content"
rows="10"
/>
<x-artisanpack-button type="submit" primary>
Save Post
</x-artisanpack-button>
</x-artisanpack-form>
2. artisanpack-ui/icons
Comprehensive icon library with easy integration.
composer require artisanpack-ui/icons
Usage:
<x-artisanpack-icon name="home" class="w-5 h-5" />
<x-artisanpack-icon name="user" size="lg" />
<x-artisanpack-icon name="settings" variant="outline" />
3. artisanpack-ui/security
Enhanced security features for production applications.
composer require artisanpack-ui/security
Key Features:
- Advanced rate limiting
- IP whitelisting/blacklisting
- Security headers management
- Vulnerability scanning
- Automated security monitoring
4. artisanpack-ui/accessibility
Ensure your CMS meets accessibility standards.
composer require artisanpack-ui/accessibility
Features:
- Accessibility testing tools
- ARIA attribute helpers
- Color contrast validation
- Screen reader optimization
- Keyboard navigation support
Essential Laravel Ecosystem Packages
Content & Media Management
1. Spatie Image
composer require spatie/image
Advanced image manipulation and optimization.
2. Laravel Translatable
composer require spatie/laravel-translatable
Multi-language content support.
3. Laravel Sluggable
composer require cviebrock/eloquent-sluggable
Automatic slug generation for content.
Performance & Caching
1. Laravel Redis
composer require predis/predis
Redis integration for caching and sessions.
2. Laravel Horizon
composer require laravel/horizon
Queue monitoring and management.
3. Laravel Telescope
composer require laravel/telescope --dev
Debug and monitoring toolbar.
Search & Filtering
1. Laravel Scout
composer require laravel/scout
Full-text search integration.
2. TNTSearch
composer require teamtnt/laravel-scout-tntsearch-driver
Lightweight search engine for Scout.
3. Algolia Scout
composer require algolia/scout-extended
Advanced search with Algolia.
API Development
1. Laravel Passport (Alternative to Sanctum)
composer require laravel/passport
OAuth2 server implementation.
2. Spatie Query Builder
composer require spatie/laravel-query-builder
API query filtering and sorting.
3. Laravel Fractal
composer require spatie/fractal
API resource transformation.
SEO & Analytics
1. Laravel Sitemap
composer require spatie/laravel-sitemap
Automatic sitemap generation.
2. Laravel Analytics
composer require spatie/laravel-analytics
Google Analytics integration.
3. Laravel Meta
composer require artesaos/seotools
SEO meta tags management.
Recommended Development Stack
For a complete CMS development environment:
Backend Stack:
- Laravel 12+
- ArtisanPack UI CMS Framework
- MySQL/PostgreSQL
- Redis
- Laravel Sanctum/Passport
Frontend Stack:
- Livewire + Alpine.js (Traditional)
- OR Inertia.js + Vue.js/React
- Tailwind CSS
- ArtisanPack UI Components
Development Tools:
- Laravel Sail (Docker)
- Laravel Telescope
- Laravel Debugbar
- Pest Testing Framework
Production Essentials:
- Laravel Horizon (Queue Management)
- Laravel Scout (Search)
- Spatie Media Library
- Security & Accessibility packages
Best Practices
Code Organization
Follow Laravel Conventions
Adhere to Laravel's coding standards and conventions:
// ✅ Good: Follow PSR standards and Laravel conventions
class PostController extends Controller
{
public function index(Request $request): JsonResponse
{
$posts = Post::query()
->published()
->with(['author', 'categories'])
->paginate(15);
return response()->json($posts);
}
}
// ❌ Avoid: Poor naming and structure
class post_controller extends Controller
{
function get_posts() {
return Post::all();
}
}
Use Type Declarations
Always use type declarations for better code clarity and IDE support:
// ✅ Good: Explicit type declarations
public function createPost(string $title, array $data): Post
{
return Post::create([
'title' => $title,
'content' => $data['content'] ?? '',
'status' => $data['status'] ?? 'draft',
]);
}
// ❌ Avoid: Missing type declarations
public function createPost($title, $data)
{
return Post::create(['title' => $title]);
}
Leverage Service Classes
Extract complex logic into dedicated service classes:
// ✅ Good: Dedicated service class
class PostService
{
public function publish(Post $post): bool
{
if (!$this->canPublish($post)) {
return false;
}
$post->update([
'status' => 'published',
'published_at' => now(),
]);
event(new PostPublished($post));
return true;
}
private function canPublish(Post $post): bool
{
return $post->title &&
$post->content &&
auth()->user()->can('publish', $post);
}
}
Security Best Practices
Input Validation
Always validate and sanitize user input:
// Form Request Validation
class CreatePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
'status' => ['required', 'in:draft,published'],
'categories' => ['array', 'exists:categories,id'],
'featured_image' => ['nullable', 'image', 'max:2048'],
];
}
public function messages(): array
{
return [
'title.required' => 'Post title is required',
'content.required' => 'Post content cannot be empty',
];
}
}
Authorization Policies
Implement comprehensive authorization policies:
class PostPolicy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('view_posts');
}
public function view(User $user, Post $post): bool
{
return $user->hasPermission('view_posts') ||
$post->author_id === $user->id;
}
public function create(User $user): bool
{
return $user->hasPermission('create_posts');
}
public function update(User $user, Post $post): bool
{
return $user->hasPermission('edit_posts') ||
($post->author_id === $user->id && $user->hasPermission('edit_own_posts'));
}
public function delete(User $user, Post $post): bool
{
return $user->hasPermission('delete_posts') ||
($post->author_id === $user->id && $user->hasPermission('delete_own_posts'));
}
}
Rate Limiting
Implement rate limiting for API endpoints:
// In RouteServiceProvider or API routes
Route::middleware(['throttle:api'])->group(function () {
Route::apiResource('posts', PostController::class);
});
// Custom rate limiting
Route::middleware(['throttle:60,1'])->group(function () {
Route::post('contact', [ContactController::class, 'store']);
});
Performance Optimization
Database Query Optimization
Optimize database queries to prevent N+1 problems:
// ✅ Good: Eager loading relationships
public function index()
{
$posts = Post::with(['author', 'categories', 'tags'])
->published()
->latest('published_at')
->paginate(10);
return view('posts.index', compact('posts'));
}
// ❌ Avoid: N+1 query problem
public function index()
{
$posts = Post::published()->paginate(10);
// This will cause N+1 queries when accessing $post->author in the view
return view('posts.index', compact('posts'));
}
Implement Caching
Use caching strategically for frequently accessed data:
class PostService
{
public function getPopularPosts(int $limit = 10): Collection
{
return Cache::remember('popular_posts', 3600, function () use ($limit) {
return Post::published()
->withCount('views')
->orderBy('views_count', 'desc')
->limit($limit)
->get();
});
}
public function getFeaturedPosts(): Collection
{
return Cache::tags(['posts', 'featured'])
->remember('featured_posts', 1800, function () {
return Post::featured()->published()->get();
});
}
public function clearPostCache(): void
{
Cache::tags(['posts'])->flush();
}
}
Use Database Indexing
Create appropriate database indexes:
// In migration files
Schema::table('posts', function (Blueprint $table) {
$table->index(['status', 'published_at']); // For published posts queries
$table->index(['author_id', 'created_at']); // For author's posts
$table->index('slug'); // For slug lookups
$table->fullText(['title', 'content']); // For search functionality
});
Testing Strategies
Feature Tests
Write comprehensive feature tests:
class PostManagementTest extends TestCase
{
use RefreshDatabase;
public function test_authenticated_user_can_create_post(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post('/api/posts', [
'title' => 'Test Post',
'content' => 'This is test content',
'status' => 'draft',
]);
$response->assertSuccessful();
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
'author_id' => $user->id,
'status' => 'draft',
]);
}
public function test_guest_cannot_create_post(): void
{
$response = $this->post('/api/posts', [
'title' => 'Test Post',
'content' => 'This is test content',
]);
$response->assertUnauthorized();
}
}
Unit Tests
Test individual components in isolation:
class PostServiceTest extends TestCase
{
use RefreshDatabase;
public function test_can_publish_valid_post(): void
{
$user = User::factory()->create();
$post = Post::factory()->create([
'author_id' => $user->id,
'status' => 'draft',
]);
$service = new PostService();
$result = $service->publish($post);
$this->assertTrue($result);
$this->assertEquals('published', $post->fresh()->status);
$this->assertNotNull($post->fresh()->published_at);
}
}
Livewire Component Tests
Test Livewire components:
class CreatePostComponentTest extends TestCase
{
use RefreshDatabase;
public function test_can_create_post_via_livewire(): void
{
$user = User::factory()->create();
Livewire::actingAs($user)
->test(CreatePost::class)
->set('title', 'New Post Title')
->set('content', 'Post content here')
->call('save')
->assertSuccessful()
->assertRedirect('/admin/posts');
$this->assertDatabaseHas('posts', [
'title' => 'New Post Title',
'author_id' => $user->id,
]);
}
}
Content Management Best Practices
Content Versioning
Implement content versioning for important content:
class Post extends Model
{
use HasVersions;
protected $versionable = [
'title', 'content', 'excerpt', 'meta_title', 'meta_description'
];
public function createVersion(): void
{
$this->versions()->create([
'data' => $this->only($this->versionable),
'created_by' => auth()->id(),
]);
}
}
SEO Optimization
Implement SEO best practices:
class SEOService
{
public function generateMetaTags(Post $post): array
{
return [
'title' => $post->meta_title ?: $post->title,
'description' => $post->meta_description ?: Str::limit(strip_tags($post->content), 160),
'keywords' => $post->tags->pluck('name')->implode(', '),
'og:title' => $post->title,
'og:description' => $post->excerpt,
'og:image' => $post->featured_image_url,
'og:url' => url("/posts/{$post->slug}"),
];
}
}
Content Validation
Validate content integrity:
class ContentValidator
{
public function validate(Post $post): array
{
$issues = [];
if (empty($post->meta_description)) {
$issues[] = 'Missing meta description';
}
if (empty($post->featured_image)) {
$issues[] = 'Missing featured image';
}
if (str_word_count($post->content) < 300) {
$issues[] = 'Content too short (minimum 300 words)';
}
return $issues;
}
}
Performance & Security
Performance Optimization
Database Performance
Query Optimization:
// Use database indexes for frequent queries
Schema::table('posts', function (Blueprint $table) {
$table->index(['status', 'published_at']);
$table->index(['author_id', 'status']);
$table->index('slug');
$table->fullText(['title', 'content']);
});
// Use pagination for large datasets
public function index(Request $request)
{
return Post::published()
->with(['author', 'categories'])
->orderBy('published_at', 'desc')
->simplePaginate(15); // More efficient than paginate()
}
// Use database aggregation instead of PHP calculations
$stats = [
'total_posts' => Post::count(),
'published_posts' => Post::where('status', 'published')->count(),
'draft_posts' => Post::where('status', 'draft')->count(),
'posts_this_month' => Post::whereMonth('created_at', now()->month)->count(),
];
Connection Pooling and Read Replicas:
// config/database.php
'connections' => [
'mysql' => [
'read' => [
'host' => ['192.168.1.1', '192.168.1.2'],
],
'write' => [
'host' => ['192.168.1.3'],
],
'sticky' => true,
// ... other config
],
],
Caching Strategies
Multi-Level Caching:
class PostService
{
public function getPost(string $slug): ?Post
{
// Level 1: Application cache
return Cache::remember("post:{$slug}", 3600, function () use ($slug) {
return Post::with(['author', 'categories', 'tags'])
->where('slug', $slug)
->published()
->first();
});
}
public function getPostsForCategory(string $categorySlug, int $page = 1): LengthAwarePaginator
{
$cacheKey = "category:{$categorySlug}:page:{$page}";
return Cache::tags(['posts', 'categories'])
->remember($cacheKey, 1800, function () use ($categorySlug, $page) {
return Post::whereHas('categories', function ($query) use ($categorySlug) {
$query->where('slug', $categorySlug);
})
->published()
->with(['author', 'categories'])
->paginate(10, ['*'], 'page', $page);
});
}
public function invalidatePostCache(Post $post): void
{
Cache::forget("post:{$post->slug}");
Cache::tags(['posts'])->flush();
// Clear category-specific caches
foreach ($post->categories as $category) {
Cache::tags(['categories'])->flush();
}
}
}
Redis Configuration:
// config/cache.php
'redis' => [
'client' => 'phpredis',
'options' => [
'cluster' => 'redis',
'prefix' => env('REDIS_PREFIX', 'cms_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
'persistent' => true,
],
];
Asset Optimization
Frontend Build Optimization:
// vite.config.js
export default defineConfig({
plugins: [laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
})],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['lodash', 'axios'],
admin: ['admin.js'],
}
}
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
}
}
},
});
Image Optimization:
class MediaService
{
public function processImage(UploadedFile $file): Media
{
$media = Media::create([
'name' => $file->getClientOriginalName(),
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
]);
// Create multiple sizes
$this->createImageVariants($file, $media);
return $media;
}
private function createImageVariants(UploadedFile $file, Media $media): void
{
$sizes = [
'thumb' => [150, 150],
'medium' => [300, 300],
'large' => [800, 600],
];
foreach ($sizes as $name => $dimensions) {
Image::make($file)
->fit($dimensions[0], $dimensions[1])
->encode('webp', 85)
->save(storage_path("app/media/{$media->id}_{$name}.webp"));
}
}
}
Security Hardening
Authentication Security
Rate Limiting:
// config/sanctum.php
'expiration' => 525600, // 1 year in minutes
// Custom rate limiting
class AuthController extends Controller
{
public function login(Request $request)
{
$this->validateLogin($request);
// Rate limit failed login attempts
if (RateLimiter::tooManyAttempts(
$this->throttleKey($request), 5
)) {
return response()->json([
'message' => 'Too many login attempts. Please try again later.'
], 429);
}
if ($this->attemptLogin($request)) {
RateLimiter::clear($this->throttleKey($request));
return $this->sendLoginResponse($request);
}
RateLimiter::hit($this->throttleKey($request), 3600);
return $this->sendFailedLoginResponse($request);
}
protected function throttleKey(Request $request): string
{
return Str::lower($request->input('email')).'|'.$request->ip();
}
}
Two-Factor Authentication:
class TwoFactorService
{
public function enable(User $user): array
{
$user->two_factor_secret = encrypt(app(TwoFactorAuthenticationProvider::class)->generateSecretKey());
$user->save();
return [
'qr_code' => app(TwoFactorAuthenticationProvider::class)->qrCodeUrl(
config('app.name'),
$user->email,
decrypt($user->two_factor_secret)
),
'recovery_codes' => $this->generateRecoveryCodes($user),
];
}
public function verify(User $user, string $code): bool
{
return app(TwoFactorAuthenticationProvider::class)->verify(
decrypt($user->two_factor_secret),
$code
);
}
}
Input Sanitization
XSS Prevention:
class ContentSanitizer
{
private array $allowedTags = [
'p', 'br', 'strong', 'em', 'ul', 'ol', 'li',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'a', 'img', 'blockquote', 'code', 'pre'
];
private array $allowedAttributes = [
'a' => ['href', 'title'],
'img' => ['src', 'alt', 'width', 'height'],
'blockquote' => ['cite'],
];
public function sanitize(string $content): string
{
return strip_tags(
$content,
'<' . implode('><', $this->allowedTags) . '>'
);
}
public function sanitizeForAdmin(string $content): string
{
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', $this->buildAllowedString());
$purifier = new HTMLPurifier($config);
return $purifier->purify($content);
}
}
CSRF and CORS Protection
CSRF Configuration:
// config/sanctum.php
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
// In forms
<form method="POST" action="/admin/posts">
@csrf
<!-- form fields -->
</form>
CORS Configuration:
// config/cors.php
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => [
'https://yourdomain.com',
'https://admin.yourdomain.com',
],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
File Upload Security
Secure File Upload:
class SecureFileUpload
{
private array $allowedMimes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf', 'text/plain',
];
private int $maxFileSize = 5 * 1024 * 1024; // 5MB
public function validate(UploadedFile $file): array
{
$errors = [];
// Check file size
if ($file->getSize() > $this->maxFileSize) {
$errors[] = 'File size exceeds maximum allowed size';
}
// Check MIME type
if (!in_array($file->getMimeType(), $this->allowedMimes)) {
$errors[] = 'File type not allowed';
}
// Check for malicious content
if ($this->containsMaliciousContent($file)) {
$errors[] = 'File contains malicious content';
}
return $errors;
}
private function containsMaliciousContent(UploadedFile $file): bool
{
$content = file_get_contents($file->getRealPath());
// Check for PHP tags in uploaded files
if (strpos($content, '<?php') !== false) {
return true;
}
// Check for suspicious patterns
$suspiciousPatterns = [
'/<script/i',
'/javascript:/i',
'/vbscript:/i',
'/onload=/i',
'/onerror=/i',
];
foreach ($suspiciousPatterns as $pattern) {
if (preg_match($pattern, $content)) {
return true;
}
}
return false;
}
}
Monitoring and Logging
Application Performance Monitoring
Custom Metrics Collection:
class PerformanceMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$startTime = microtime(true);
$startMemory = memory_get_usage();
$response = $next($request);
$executionTime = microtime(true) - $startTime;
$memoryUsed = memory_get_usage() - $startMemory;
// Log slow queries
if ($executionTime > 0.5) {
Log::warning('Slow request detected', [
'url' => $request->fullUrl(),
'method' => $request->method(),
'execution_time' => $executionTime,
'memory_used' => $memoryUsed,
'user_id' => auth()->id(),
]);
}
// Store metrics
Cache::increment('requests_total');
Cache::increment('requests_by_route:' . $request->route()->getName());
return $response;
}
}
Health Check Endpoint:
class HealthController extends Controller
{
public function check(): JsonResponse
{
$checks = [
'database' => $this->checkDatabase(),
'cache' => $this->checkCache(),
'storage' => $this->checkStorage(),
'queue' => $this->checkQueue(),
];
$healthy = collect($checks)->every(fn($check) => $check['status'] === 'ok');
return response()->json([
'status' => $healthy ? 'healthy' : 'unhealthy',
'checks' => $checks,
'timestamp' => now()->toISOString(),
], $healthy ? 200 : 503);
}
private function checkDatabase(): array
{
try {
DB::connection()->getPdo();
return ['status' => 'ok', 'message' => 'Database connection successful'];
} catch (Exception $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
}
Security Monitoring
Audit Logging:
class AuditLogger
{
public function logUserActivity(string $action, Model $model = null, array $changes = []): void
{
AuditLog::create([
'user_id' => auth()->id(),
'action' => $action,
'model_type' => $model ? get_class($model) : null,
'model_id' => $model?->id,
'changes' => $changes,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'created_at' => now(),
]);
}
public function logSecurityEvent(string $event, array $context = []): void
{
Log::channel('security')->warning($event, array_merge($context, [
'user_id' => auth()->id(),
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'timestamp' => now()->toISOString(),
]));
}
}
Deployment
Production Server Setup
Server Requirements
Minimum Server Specifications:
- CPU: 2 cores (4 cores recommended)
- RAM: 2GB (4GB+ recommended)
- Storage: 20GB SSD
- PHP: 8.2+ with required extensions
- Web Server: Nginx or Apache
- Database: MySQL 8.0+ or PostgreSQL 13+
- Redis: 6.0+ for caching and sessions
Server Configuration
Nginx Configuration:
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
root /var/www/html/public;
# SSL Configuration
ssl_certificate /etc/ssl/certs/yourdomain.com.pem;
ssl_certificate_key /etc/ssl/private/yourdomain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Basic Configuration
index index.php;
charset utf-8;
client_max_body_size 20M;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Handle Laravel Routes
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM Configuration
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
# Security: Block access to sensitive files
location ~ /\.(?!well-known).* {
deny all;
}
# Static Asset Caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Docker Deployment:
FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
nginx \
supervisor \
redis \
mysql-client \
freetype-dev \
libjpeg-turbo-dev \
libpng-dev \
libzip-dev \
icu-dev \
oniguruma-dev
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql \
mysqli \
zip \
gd \
intl \
mbstring \
bcmath \
opcache \
pcntl
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application code
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader
# Set permissions
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage
EXPOSE 80
CMD ["php-fpm"]
Troubleshooting
Common Installation Issues
Database Connection Problems
Error: SQLSTATE[HY000] [2002] Connection refused
Solutions:
# Check database service status
sudo systemctl status mysql
sudo systemctl start mysql
# Verify database credentials
mysql -u username -p database_name
# Check Laravel database configuration
php artisan config:clear
php artisan config:cache
Error: Access denied for user
Solutions:
-- Create database user with proper permissions
CREATE USER 'cms_user'@'localhost' IDENTIFIED BY 'strong_password';
CREATE DATABASE cms_database;
GRANT ALL PRIVILEGES ON cms_database.* TO 'cms_user'@'localhost';
FLUSH PRIVILEGES;
Permission Issues
Error: The stream or file could not be opened in append mode
Solutions:
# Fix storage permissions
sudo chown -R www-data:www-data storage/
sudo chown -R www-data:www-data bootstrap/cache/
sudo chmod -R 775 storage/
sudo chmod -R 775 bootstrap/cache/
# Create storage symlink
php artisan storage:link
# Clear all caches
php artisan cache:clear
php artisan config:clear
php artisan view:clear
Memory Limit Issues
Error: Fatal error: Allowed memory size exhausted
Solutions:
; In php.ini
memory_limit = 512M
max_execution_time = 300
max_input_vars = 3000
; For specific operations
php -d memory_limit=512M artisan migrate
Performance Issues
Slow Page Load Times
Diagnostic Steps:
// Enable query logging temporarily
DB::enableQueryLog();
// Your code here
dd(DB::getQueryLog());
// Check for N+1 queries
php artisan telescope:install
Common Solutions:
// Use eager loading
$posts = Post::with(['author', 'categories', 'tags'])->get();
// Implement caching
Cache::remember('popular_posts', 3600, function () {
return Post::popular()->limit(10)->get();
});
// Use pagination
$posts = Post::paginate(15);
High Memory Usage
Solutions:
// Use chunking for large datasets
Post::chunk(1000, function ($posts) {
foreach ($posts as $post) {
// Process post
}
});
// Use cursors for memory-efficient iteration
foreach (Post::cursor() as $post) {
// Process post
}
// Clear Eloquent model cache
Model::clearBootedModels();
Security Issues
CSRF Token Mismatch
Solutions:
{{-- Ensure CSRF token is included --}}
<form method="POST">
@csrf
{{-- form fields --}}
</form>
{{-- For AJAX requests --}}
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
</script>
Session Issues
Solutions:
// config/session.php
'same_site' => 'lax',
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
// Clear sessions
php artisan session:table
php artisan migrate
API Issues
Authentication Problems
Solutions:
// Ensure Sanctum is properly configured
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
// Check API routes
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
CORS Issues
Solutions:
// config/cors.php
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:3000'],
'supports_credentials' => true,
Development Tools
Debugging Commands
# Check application status
php artisan cms:status
# Clear all caches
php artisan optimize:clear
# Run diagnostics
php artisan config:show database
php artisan route:list
php artisan view:cache
# Check logs
tail -f storage/logs/laravel.log
# Run tests with coverage
php artisan test --coverage
Performance Profiling
// Add to AppServiceProvider for development
if (app()->environment('local')) {
app(Illuminate\Contracts\Http\Kernel::class)
->pushMiddleware(Barryvdh\Debugbar\Middleware\DebugbarEnabled::class);
}
Conclusion
This comprehensive guide has walked you through creating a production-ready content management system using the ArtisanPack UI CMS Framework. You've learned:
- Core Architecture: Understanding the modular framework structure
- Step-by-Step Implementation: Building a complete CMS from scratch
- Advanced Features: Implementing themes, plugins, APIs, and PWA capabilities
- Best Practices: Following Laravel conventions and security guidelines
- Performance Optimization: Scaling your CMS for production use
- Deployment Strategies: Getting your CMS live with confidence
The ArtisanPack UI CMS Framework provides a solid foundation for building sophisticated content management systems that can grow with your needs. By following the patterns and practices outlined in this guide, you'll be able to create maintainable, secure, and performant CMS applications.
For additional support and community discussions, visit:
Happy coding! 🚀