kreetancraft / laravel-blog
Blog for Laravel — posts, categories, tags, authors, series and moderated comments, with SEO and a public read API. Livewire 4 + Flux UI.
Requires
- php: ^8.2
- kreetancraft/laravel-seo: ^0.1
- laravel/framework: ^12.0|^13.0
- livewire/flux: ^2.0
- livewire/livewire: ^4.0
- lorisleiva/laravel-actions: ^2.7
- sandermuller/laravel-fluent-validation: ^1.30
- spatie/laravel-query-builder: ^6.0|^7.0
- spatie/laravel-sluggable: ^3.7
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- spatie/laravel-permission: ^6.0|^7.0|^8.0
Suggests
- kreetancraft/laravel-media-manager: Featured images, author avatars and series covers, via blog.image_resolver.
- kreetancraft/laravel-user-management: Discovers this package's policies and creates their permissions automatically.
Provides
None
Conflicts
None
Replaces
None
README
Posts, categories, tags, authors, series and moderated comments, with SEO metadata and a public read API. Livewire 4 + Flux UI.
Ships no layout, no CSS, no user model and no image handling — it renders into your application and works with whatever you already have.
Design decisions worth knowing before you install
It does not care which user model you have. Comments resolve their author through
config('auth.providers.users.model'); every policy type-hints Authenticatable.
It names no permission of its own. Its screens ask the ordinary authorization question and its policies answer it. Until permissions exist anywhere in the app they are open, so it works on a bare install rather than failing closed.
It ships no image handling. Featured images, author avatars and series covers resolve
through blog.image_resolver. With none configured they return null and the pickers hide
themselves — the blog works, it just has no pictures.
Flux is a hard dependency. The views use <flux:*> throughout.
Installation
composer require kreetancraft/laravel-blog
php artisan migrate
Let Tailwind see this package
Required. Tailwind v4 generates only the classes it finds by scanning files, and it does not scan
vendor/. In resources/css/app.css:
@source '../../vendor/kreetancraft/laravel-blog/resources/views'; @source '../../vendor/kreetancraft/laravel-seo/resources/views';
Skipping it fails confusingly rather than loudly — classes shared with your own views still work and only the ones unique to these packages go missing.
The editor
A TipTap editor ships with the package, bundled — no npm packages, no build step, nothing to
publish. It is served from the package itself, so upgrading is composer update alone.
<x-blog::rich-text model="content" :label="__('Body')" :rows="20" />
What it does: bold, italic, underline, strikethrough, three heading levels, bullet, numbered and task lists, blockquote, code block, links, images, text alignment, colour and highlight, horizontal rule, clear formatting, undo/redo, source view and fullscreen — plus:
| Tables | Insert, resizable columns, add/remove rows and columns, header row, merge and split cells |
| Callouts | Note, tip, warning and danger, as <div data-callout="…"> so a sanitiser leaves them alone |
| YouTube embeds | Paste a URL; served cookie-free |
| Find and replace | Across the whole document, with match case |
| Word and character count | Live, under the editor |
| Slash menu | Type / at the start of a block for a filtered command palette — headings, lists, table, image, video, callouts, divider. Arrow keys and Enter, or click |
| Paste cleanup | Word and Google Docs paste a document, not a fragment — mso-* styles, <o:p> tags and a class on every element. Structure is kept, presentation dropped |
The editor mounts its own picker through blog.media_picker_modal_view, defaulting to
media::picker-modal. That is a modal only — the toolbar button is the trigger, so nothing
visible is added to the page. It dispatches media-picked for the rich-text-image group and
inserts what it receives.
Name a view that does not exist and nothing is mounted: the image button is inert rather than
erroring. blog.media_picker_view is separate and is the field used for featured images,
avatars and covers.
To replace the editor entirely, publish the views and edit
resources/views/vendor/blog/components/rich-text.blade.php, or set
blog.routes.serve_assets to false and load your own.
Publishing the views freezes them.
vendor:publish --tag=blog-viewscopies these screens into your application, and a published copy wins over the package's — so upgrades stop reaching them. That is how a stray image card survived a release that removed it. Publish to restyle, by all means; just republish with--forceafter upgrading, or expect to merge by hand.
Permissions
Every policy declares a subject, so with kreetancraft/laravel-user-management installed one command creates all of them:
php artisan user-management:sync-permissions
| Subject | Abilities |
|---|---|
post |
view, create, update, delete, publish |
blog-comment |
view, create, update, delete, moderate |
blog-category |
view, create, update, delete |
blog-tag |
view, create, update, delete |
blog-author |
view, create, update, delete |
blog-series |
view, create, update, delete |
Publishing is separate from editing on purpose: a writer may draft without being able to put it
in front of readers. The taxonomy subjects are prefixed blog- because category and tag are
words other packages will want too.
Posts and Comments links also appear in that package's sidebar, with nothing declared on either side.
SEO
kreetancraft/laravel-seo is a hard dependency — posts, categories, authors and series all carry
meta, JSON-LD and sitemap entries. This package contributes itself to that one through container
tags, so neither names the other in config:
- its four SEO-enabled models appear on the bulk SEO screen
- its URLs appear in
GET /api/v1/sitemap BlogJsonLd::posting()buildsBlogPostingschema from the site-wide publisher node
Front-end paths default to /blog/{slug}, /blog/category/{slug} and so on. Override any of
them in config/seo.php under paths.
Images
Install the media package and point two config values at it. That is the whole setup:
composer require kreetancraft/laravel-media-manager
// config/blog.php 'image_resolver' => \Kreetancraft\Media\Support\MediaImageResolver::class, 'media_picker_view' => 'media::picker-field',
// config/seo.php — the same field, for og:image 'og_picker_view' => 'media::picker-field',
Featured images, author avatars and series covers now show a Choose button on their edit pages, and the rich editor's image button opens the same library. Nothing else to write.
Where images appear once wired
| Post edit | Featured image, and images inserted into the body from the editor |
| Author edit | Avatar |
| Series edit | Cover |
| Category edit | og:image, through the SEO panel |
| Anywhere with the SEO panel | og:image, previewed live in the Facebook and X cards |
How it works. Neither package depends on the other. The blog asks a configured resolver for URLs and renders a configured view for picking; the media package supplies both. Point them at your own class and view instead and nothing here changes.
Leave both null and the blog still runs — images return null and the pickers hide themselves rather than rendering a button with nothing behind it.
Listings call Post::preloadImages() once per page, so images cost one query for the page rather
than two per row.
The public API
Read-only, and only published posts:
GET /api/v1/posts index, filterable by category, tag, author, series, featured
GET /api/v1/posts/{slug} detail, with JSON-LD, neighbours, related and approved comments
GET /api/v1/categories GET /api/v1/categories/{slug}
GET /api/v1/authors/{slug} GET /api/v1/series/{slug}
GET /api/v1/tags
POST /api/v1/posts/{slug}/comments always lands in the moderation queue as pending
Rate limiters are configurable and unset by default — naming one your app has not defined would throw on routes this package registers automatically:
'api_rate_limiter' => 'api-read', 'api_write_rate_limiter' => 'api-sensitive',
Set blog.comments.enabled to false and the comment endpoint is not registered at all.
Scheduled publishing
php artisan blogs:publish-scheduled
Schedule it in routes/console.php; posts with a scheduled status and a due published_at
become published and the API cache is flushed.
Requirements
PHP 8.2+, Laravel 12 or 13, Livewire 4, Flux 2, and kreetancraft/laravel-seo.
License
MIT.