dominservice / laravel-cms
Laravel CMS Backend
Requires
- php: >=8.0
- ext-gd: *
- astrotomic/laravel-translatable: ^11.13
- kalnoy/nestedset: ^6.0
- laravel/framework: ^9|^10|^11|^12
- dev-main
- 3.6.0
- 3.5.3
- 3.5.2
- 3.5.1
- 3.5.0
- 3.4.5
- 3.4.4
- 3.4.3
- 3.4.2
- 3.4.1
- 3.4.0
- 3.3.12
- 3.3.11
- 3.3.10
- 3.3.9
- 3.3.8
- 3.3.7
- 3.3.6
- 3.3.5
- 3.3.4
- 3.3.3
- 3.3.2
- 3.3.1
- 3.3.0
- 3.2.2
- 3.2.1
- 3.2.0
- 3.1.1
- 3.1.0
- 3.0.0
- 2.5.2
- 2.5.1
- 2.5.0
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.0
- 2.2.5
- 2.2.4
- 2.2.3
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.1
- 2.1.0
- 2.0.1
- 2.0.0
- 1.1.1
- 1.1.0
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
This package is auto-updated.
Last update: 2025-10-01 05:32:55 UTC
README
A complete CMS package for Laravel (9–12) that provides:
- Models and migrations for contents and categories (multilingual, nested categories tree),
- File metadata (avatar and arbitrary file kinds),
- Video avatars and posters (thumbnails extracted from video),
- Flexible configuration of sizes, file-kind mapping and disks,
- NEW: Content-to-Content Links with time visibility window and metadata,
- NEW: Content metadata as an object (Content.meta JSON cast).
This README is a full rewrite in English, expanded with practical examples, a v2→v3 migration guide, and a step-by-step “Build your own CMS in Laravel (WordPress alternative)” walkthrough.
Table of contents
- Requirements
- Installation
- Publishing and migrations
- Configuration (config/cms.php) — detailed with examples
- Tables
- Disks
- Image extension (avatar.extension)
- Files and sizes (files.*)
- Mappings: file_kind_map and file_config_key_map (ready-to-use scenarios)
- Date/time formats (date_format/time_format)
- Models, traits and relations
- Content, Category
- ContentFile, CategoryFile (file metadata)
- ContentLink (NEW) and the HasContentLinks trait
- DynamicAvatarAccessor (images, videos, posters, dynamic accessors)
- Content metadata as an object (NEW)
- Media Helper (image upload and processing)
- uploadModelImage
- uploadModelImageWithDefaults
- Notes about videos and posters
- Usage examples (extended)
- Categories and contents (multilingual)
- Image upload and URL retrieval
- Video and poster
- Content ↔ Content links (NEW)
- Advanced mappings and multiple disks
- Migration from v2 to v3 (checklist + snippet)
- Build your own CMS (WordPress alternative) — step by step
- Backward compatibility and fallbacks
- FAQ / Troubleshooting
- Contributing & Collaboration
- Support this project (Ko‑fi)
- License
Requirements
- PHP >= 8.0
- Laravel 9.x, 10.x, 11.x or 12.x
- astrotomic/laravel-translatable ^11.13 (multilingual)
- kalnoy/nestedset ^6.0 (nested tree)
- PHP ext-gd (image processing)
Installation
- Install the package:
composer require dominservice/laravel-cms
- The ServiceProvider is auto‑discovered (Laravel Package Discovery).
Publishing and migrations
- Publish configuration:
php artisan vendor:publish --provider="Dominservice\\LaravelCms\\ServiceProvider" --tag=config
- Publish migrations (CMS tables, files metadata, content links, etc.):
php artisan vendor:publish --provider="Dominservice\\LaravelCms\\ServiceProvider" --tag=migrations
- Run migrations:
php artisan migrate
Published migrations include (may vary by version):
- create_cms_tables
- add_columns_content_table
- create_redirects_table
- add_columns_category_content_table
- add_columns_categoryies_table
- create_video_table
- add_column_content_table
- create_files_tables (cms_content_files, cms_category_files)
- add_external_url_to_contents_table
- add_parent_to_content_table
- create_cms_content_links_table (NEW — content ↔ content links)
- add_meta_to_contents_table (NEW — adds a JSON meta column to contents)
Configuration (config/cms.php) — highlights Check the published file to see the current values. Key options:
- tables — table names used by the package, e.g., cms_contents, cms_categories, cms_content_files, cms_category_files, cms_content_links, etc.
- disks — maps entity → Storage disk.
- Multiple disks example: define disks in config/filesystems.php (S3, CDN-backed public, etc.), then in cms.php:
'disks' => [ 'content' => 'public', // images for contents 'content_video' => 's3', // videos go to S3 'category' => 'public', ],
- Multiple disks example: define disks in config/filesystems.php (S3, CDN-backed public, etc.), then in cms.php:
- avatar.extension — output image extension (webp by default).
- files — definition of file types and their sizes per entity:
- files.content.types.avatar.display — which size is returned by $model->avatar_path,
- files.content.types.avatar.sizes — list of sizes to generate (except 'original' => null which is skipped on upload),
- video_avatar (video) and video_poster (image poster) for Content,
- analogous sections for Category,
- you can add your own kinds, e.g., 'gallery', 'document', 'banner'.
- file_kind_map — map logical names (avatar, video_avatar, video_poster) to actual kind values stored in *_files.
- Scenario: migrating from custom kind names. You want the accessor to look for 'hero' first, then 'avatar':
'file_kind_map' => [ 'avatar' => ['hero', 'avatar'], 'video_avatar' => ['movie', 'video_avatar'], 'video_poster' => ['movie_poster', 'video_poster'], ],
- Scenario: migrating from custom kind names. You want the accessor to look for 'hero' first, then 'avatar':
- file_config_key_map — map a kind to the configuration key (and disk) used to build URLs.
- Globally or per-entity:
'file_config_key_map' => [ // global 'avatar' => 'content', 'video_poster' => 'content', 'video_avatar' => 'content_video', // scoped by base key (content/category) 'content' => [ 'test' => 'test_123', // when ContentFile.kind = 'test' → use config key 'test_123' ], ],
- Globally or per-entity:
- date_format, time_format — formats used by date accessors.
Models, traits and relations
-
Content (Dominservice\LaravelCms\Models\Content)
- Multilingual (Astrotomic Translatable), SoftDeletes.
- Relations: categories() (MTM), children()/parent(), files(), avatarFile(), video()/videoPoster() (aliases for files), rootCategory().
- Accessors: avatar_path, dynamic sizes {size}_avatar_path, video_path / {size}_video_avatar_path, video_poster_path / {size}_video_poster_path.
- external_url normalization (empty → null; trimmed on set/get).
- NEW: meta attribute cast to object (see next section).
-
Category (Dominservice\LaravelCms\Models\Category)
- Multilingual, nested tree (Nestedset), SoftDeletes.
- Relations: contents() (MTM), files(), avatarFile().
-
ContentFile and CategoryFile
- Store file metadata: kind, type ('image'|'video'), names (JSON: size → filename). You can add custom kinds (e.g., 'gallery').
-
ContentLink (NEW) and HasContentLinks trait
- Table: cms_content_links; columns: from_uuid, to_uuid, relation (optional), position, meta (JSON), visible_from, visible_to.
- Trait API: links(), backlinks(), linksOf(), backlinksOf(), visibleLinks(), attachLink(), detachLink().
DynamicAvatarAccessor (images, videos, posters)
- Provides URLs according to configuration; adds cache-busting (?v=mtime); per-request cache.
- Fallbacks: legacy naming for avatars/posters (content_{uuid}.webp, {uuid}.webp).
- Mappings: file_kind_map and file_config_key_map let you rename kinds and move disks without changing model code.
Content metadata as an object (NEW)
- The Content model defines a JSON column meta casted to an object:
// Dominservice\\LaravelCms\\Models\\Content protected $casts = [ 'external_url' => 'string', 'meta' => 'object', // <— NEW: access as $content->meta (stdClass) ];
- Migration: publish and run the migration that adds the meta column to the contents table:
php artisan vendor:publish --provider="Dominservice\\LaravelCms\\ServiceProvider" --tag=migrations php artisan migrate
Look for a migration named similar to add_meta_to_contents_table. - Usage examples:
$content = Content::create(['type' => 'article', 'status' => 1]); // Set as array (will be encoded to JSON and cast to object on read): $content->meta = [ 'reading_time' => 5, 'featured' => true, 'tags' => ['laravel','cms'], ]; $content->save(); // Read as object $meta = $content->meta; // stdClass $isFeatured = $meta->featured ?? false; $tags = $meta->tags ?? []; // Update one field safely $meta = (array) $content->meta; // cast to array if you need to mutate $meta['reading_time'] = 6; $content->meta = $meta; $content->save();
Media Helper (image upload and processing)
- uploadModelImage(Model $model, UploadedFile|string $source, string $kind = 'avatar', ?string $type = null, ?array $onlySizes = null, bool $replaceExisting = true)
- Generates sizes based on config (skips 'original' => null), writes to disk, updates *_files.
- When replaceExisting is true, removes previous variants.
- uploadModelImageWithDefaults(Model $model, array $sourcesBySize, ...)
- 'default' plus per-size overrides (e.g., separate file only for 'thumb').
- Note: responsive profiles (mobile/desktop) on upload are intentionally disabled — stick to configured sizes.
- Videos: 'video_avatar' expects ready-made files (no transcoding). 'video_poster' is a regular image.
Usage examples (extended)
- Categories and contents (create with translations)
use Dominservice\\LaravelCms\\Models\\Category; use Dominservice\\LaravelCms\\Models\\Content; $cat = Category::create(['type' => 'section', 'status' => 1]); $cat->translateOrNew('en')->name = 'News'; $cat->translateOrNew('en')->slug = 'news'; $cat->save(); $content = Content::create(['type' => 'article', 'status' => 1]); $content->translateOrNew('en')->name = 'First post'; $content->translateOrNew('en')->slug = 'first-post'; $content->save(); $content->categories()->attach($cat->uuid);
- Image upload and URL retrieval (controller + view)
use Dominservice\\LaravelCms\\Helpers\\Media; use Illuminate\\Http\\Request; public function store(Request $request) { $content = Content::create(['type' => 'article', 'status' => 1]); $content->translateOrNew('en')->fill([ 'name' => (string) $request->string('name'), 'slug' => (string) $request->string('slug'), ]); $content->save(); if ($request->hasFile('avatar')) { Media::uploadModelImage($content, $request->file('avatar'), 'avatar'); } return redirect()->route('content.show', $content->uuid); }
View (Blade):
<img src="{{ $content->avatar_path }}" alt="{{ $content->name }}" /> <img src="{{ $content->thumb_avatar_path }}" alt="Thumbnail" />
- Video and poster
// Media::uploadModelVideos($content, ['hd' => $fileHd, 'sd' => $fileSd], 'video_avatar'); // conceptual example Media::uploadModelImage($content, request()->file('poster'), 'video_poster'); $video = $content->video_avatar_path; // e.g., hd $poster = $content->video_poster_path; // e.g., large $mobileVideo = $content->mobile_video_avatar_path; // dynamic accessor if 'mobile' exists in names
- Content ↔ Content links (NEW)
use Dominservice\\LaravelCms\\Models\\Content; $a = Content::first(); $b = Content::find($someUuid); $a->attachLink($b, [ 'relation' => 'related', 'position' => 10, 'meta' => ['note' => 'editorial relation'], 'visible_from' => now(), ]); $visible = $a->visibleLinks('related')->get(); $incoming = $a->backlinks()->get();
- Advanced mappings and multiple disks
- Serve avatars from an 'images' disk and videos from 's3':
// config/cms.php 'disks' => [ 'content' => 'images', 'content_video' => 's3', ], 'file_config_key_map' => [ 'video_avatar' => 'content_video', 'video_poster' => 'content', ],
- Rename DB kinds without touching code (configuration only):
'file_kind_map' => [ 'avatar' => ['hero', 'avatar'], ],
Migration from v2 to v3 (checklist + snippet) Goal: move from v2 to v3 while keeping URLs and data intact.
Checklist:
- Update the package to v3 and publish new config and migrations:
composer require dominservice/laravel-cms:^3 php artisan vendor:publish --provider="Dominservice\\LaravelCms\\ServiceProvider" --tag=config php artisan vendor:publish --provider="Dominservice\\LaravelCms\\ServiceProvider" --tag=migrations php artisan migrate
- Configure disks in config/cms.php (especially content_video if video should live elsewhere).
- Set file_kind_map if v2 used custom kind values in DB/files:
'file_kind_map' => [ 'avatar' => ['avatar'], 'video_avatar' => ['video_avatar'], 'video_poster' => ['video_poster'], ],
- Set file_config_key_map so that videos are served from another disk if needed:
'file_config_key_map' => [ 'video_avatar' => 'content_video', 'video_poster' => 'content', ],
- File metadata (cms_content_files, cms_category_files):
- v3 prefers metadata (names JSON). If you only have legacy files (e.g., content_{uuid}.webp), the accessor still finds them, but we recommend migrating to metadata.
Migration helper snippet (one‑off console example) which reads a legacy file as 'large' and saves it into *_files without removing the original:
use Dominservice\\LaravelCms\\Models\\Content; use Dominservice\\LaravelCms\\Models\\ContentFile; use Illuminate\\Support\\Facades\\Storage; Artisan::command('cms:migrate-avatars-legacy', function () { $diskKey = config('cms.disks.content'); $ext = ltrim((string)config('cms.avatar.extension', 'webp'), '.'); Content::query()->chunk(200, function ($chunk) use ($diskKey, $ext) { foreach ($chunk as $c) { $uuid = $c->uuid; $candidates = ["content_{$uuid}.{$ext}", "content{$uuid}.{$ext}", "{$uuid}.{$ext}"]; foreach ($candidates as $name) { if (Storage::disk($diskKey)->exists($name)) { ContentFile::firstOrCreate( ['content_uuid' => $uuid, 'kind' => 'avatar'], ['type' => 'image', 'names' => ['large' => $name]] ); break; } } } }); $this->info('Legacy avatars backfilled into metadata.'); });
- If v2 stored video as a single file (video_{uuid}.mp4), in v3 you can:
- leave it as a fallback (the accessor may detect it in some scenarios),
- or create ContentFile(kind='video_avatar', type='video', names=['hd' => 'video_{uuid}.mp4']).
- Dynamic properties in v3: you can access {size}_avatar_path, {size}_video_avatar_path, {size}_video_poster_path — update views if you want specific sizes.
Build your own CMS (WordPress alternative) — step by step Below is a minimal publishing flow you can grow into a full CMS.
- Routing (public + admin)
- Public (e.g., routes/web.php):
use App\\Http\\Controllers\\ContentController; Route::get('/{slug}', [ContentController::class, 'show'])->name('content.show');
- Admin (e.g., routes/admin.php): CRUD for contents and categories (Filament/Nova/Voyager work well since the models are standard Eloquent).
- Content controller (public)
namespace App\\Http\\Controllers; use Dominservice\\LaravelCms\\Models\\Content; class ContentController extends Controller { public function show(string $slug) { $content = Content::whereTranslation('slug', $slug)->firstOrFail(); $links = $content->visibleLinks()->get(); return view('content.show', compact('content','links')); } }
- Admin form (create post)
- Validate translation fields (name, slug), status, type, and 'avatar' image.
- After save:
Media::uploadModelImage($content, $request->file('avatar'), 'avatar');
- Categories and navigation
- Build a tree with Category and the MTM relation. For menus, fetch categories of type 'section' and render according to the nested set.
- SEO and internal linking
- Use ContentLink for "related posts", "see also", "featured" blocks.
- Set meta_title/meta_description in translations.
- Media and performance
- Keep large videos on S3 (file_config_key_map → 'content_video').
- Serve images from a CDN (configure the URL for the 'public' or a dedicated 'images' disk).
- Migrating from WordPress (conceptual)
- Export posts/pages to CSV/JSON.
- Import into Content (type = 'post'|'page').
- Convert media: upload to disk and create entries in *_files (avatar/gallery/banner).
- Use redirects (cms_redirects) to preserve legacy URLs.
Backward compatibility and fallbacks
- Avatar and poster: if *_files metadata is missing, the trait checks legacy names on disk (prefix + uuid + extension) and returns their URL when present.
- Video: for Content, legacy naming (video_{uuid}.mp4|webm) is supported as a fallback (helpers are available to clean old files when replaceExisting is used).
- Dynamic properties (e.g., small_avatar_path) are handled gracefully — if a specific size is missing, the trait tries other variants and profiles.
FAQ / Troubleshooting
- No URL in avatar_path — check the _files record with kind = 'avatar' and the file presence on the disk pointed by config('cms.disks.').
- Missing sizes — complete config('cms.files.{entity}.types.{kind}.sizes') or use uploadModelImageWithDefaults.
- Issues with video/poster — verify file_config_key_map for 'video_avatar' and 'video_poster' and the 'content_video' disk settings.
- Multilingual — remember to use translateOrNew('locale') and run translation migrations.
- v2→v3 migration — follow the checklist and snippet above; publish migrations and configure mappings first.
Contributing & Collaboration
- Contributions, ideas, and collaboration offers are very welcome. Feel free to open issues and pull requests.
- If you want to discuss a feature or commercial collaboration, please reach out via GitHub issues.
Support this project (Ko‑fi) If this package saves you time, consider buying me a coffee: https://ko-fi.com/dominservice — thank you!
License MIT