ojessecruz/simple-blog
| Install | |
|---|---|
composer require ojessecruz/simple-blog |
|
| Latest Version: | v0.7.5 |
| PHP: | ^8.3 |
| License: | MIT |
| Last Updated: | Jun 11, 2026 |
| Links: | GitHub · Packagist |
Simple Blog
A ready-to-use Laravel blog — minimal reading-focused public listing, admin CRUD with Livewire, Markdown with HTML escaping, and draft preview in a new tab. Zero opinions on authentication: you plug it in via Laravel middleware (Gate, guard, or any combo).
Requirements
- PHP 8.3+
- Laravel 11 / 12 / 13
- Livewire 3.5+ or 4
- Tailwind CSS in the consuming app (the package ships Tailwind classes, it does not compile its own CSS)
Installation
composer require ojessecruz/simple-blog
Quick install (recommended)
One command publishes the config & migrations and wires Tailwind up for you:
php artisan simple-blog:install
It is idempotent — safe to re-run — and will:
- publish
config/blog.phpand the migrations; - add the Tailwind source
@importtoresources/css/app.css(Tailwind v4), right after your@import "tailwindcss";; - offer to run the migrations.
Afterwards, protect the admin routes (see below) and run npm run build. To wire things up by hand instead, follow the manual steps.
Manual installation
Publish and run the migrations:
php artisan vendor:publish --tag="simple-blog-migrations"
php artisan migrate
Publish the config (optional, but recommended):
php artisan vendor:publish --tag="simple-blog-config"
Publish the views (optional, only if you want to customize):
php artisan vendor:publish --tag="simple-blog-views"
Make Tailwind aware of the package views, otherwise it won't compile the classes they use and the blog renders unstyled.
Tailwind v4 (CSS-based config) — import the package's CSS entrypoint from your app's main stylesheet. It registers the package views as a source, and because the path is resolved relative to the package file, your app never has to know the package's internal structure:
/* resources/css/app.css */
@import 'tailwindcss';
@import '../../vendor/ojessecruz/simple-blog/resources/css/simple-blog.css';
Tailwind v3 (tailwind.config.js) — add the views to the content array:
content: [
// ... your existing paths ...
'./vendor/ojessecruz/simple-blog/resources/views/**/*.blade.php',
],
If you published the views (--tag="simple-blog-views"), they live in resources/views/ and are already covered by your app's default Tailwind sources — but keep the import above if you rely on any non-published views (e.g. the admin panel).
Quickstart
Three steps and your blog is up.
1. Protect the admin routes
The package does not embed any authorization logic. You plug it in via middleware in config/blog.php. Examples:
// Via a specific Gate
'admin_middleware' => ['web', 'auth', 'can:manage-blog'],
// Via a separate guard
'admin_middleware' => ['web', 'auth:admin'],
// Combo of your app's own middleware
'admin_middleware' => ['web', 'auth', 'verified', 'super.admin'],
If you go with a Gate, define it as usual in AuthServiceProvider:
Gate::define('manage-blog', fn ($user) => $user->is_admin === true);
2. Configure the author model
Point it to your app's User:
// config/blog.php
'author_model' => App\Models\User::class,
And make User implement the Author contract:
use Jessecruz\SimpleBlog\Contracts\Author;
class User extends Authenticatable implements Author
{
public function getBlogAuthorName(): string
{
return $this->name;
}
public function getBlogAuthorInitials(): string
{
$words = preg_split('/\s+/', trim($this->name)) ?: [];
$initials = array_map(
fn (string $w) => mb_strtoupper(mb_substr($w, 0, 1)),
array_slice($words, 0, 2),
);
return implode('', $initials);
}
public function getBlogAuthorAvatarUrl(): ?string
{
return $this->avatar_url; // or null if you don't have avatars
}
}
3. Access it
- Public:
https://yourapp.com/blog - Admin:
https://yourapp.com/admin/blog
Done.
Routes
The package registers:
| Method | URL | Name | Description |
|---|---|---|---|
| GET | /blog |
blog.index |
Public listing |
| GET | /blog/category/{slug} |
blog.category |
Posts in a category |
| GET | /blog/{slug} |
blog.show |
Individual post |
| GET | /admin/blog |
blog.admin.index |
Admin list (filter/search) |
| GET | /admin/blog/create |
blog.admin.create |
Create form |
| GET | /admin/blog/{slug}/edit |
blog.admin.edit |
Edit form |
| GET | /admin/blog/{slug}/preview |
blog.admin.preview |
Preview a draft/scheduled post |
| GET | /admin/blog/categories |
blog.admin.categories |
Categories CRUD |
The /blog and /admin/blog prefixes are configurable (route_prefix, admin_route_prefix).
Configuration
See config/blog.php (published) — every key has comments explaining what it does, with examples. Summary:
route_prefix/admin_route_prefix— where to mount the routespublic_middleware/admin_middleware— middleware stacksauthor_model— User modellayouts.public/layouts.admin— Blade layouts wrapping the contentcta_view— optional view rendered at the end of each post (e.g. pricing, newsletter)public_back_url/admin_back_url— where the "back" links point (nullhides them)markdown— options passed toStr::markdown()
Customizing the layout
The package ships two neutral default layouts, both pulled from config/blog.php:
- public (
blog::layouts.public) — used for/blogand/blog/{slug}. Plain HTML, no nav, content rendered through@yield('content'). - admin (
blog::layouts.admin) — used for/admin/blog. Slot-based, content rendered through{{ $slot }}so it can be pointed straight at modern Laravel layouts.
Most apps want the public side wrapped in their site chrome (header, nav, footer, SEO meta) and the admin inside their auth shell.
Public layout: build it from scratch
The simplest path is creating a dedicated layout for the blog with @yield('content'):
{{-- resources/views/layouts/blog-public.blade.php --}}
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', config('app.name'))</title>
@stack('head') {{-- Required: lets the package emit SEO meta and JSON-LD --}}
@vite('resources/css/app.css')
</head>
<body class="bg-white text-gray-900">
<header class="border-b">
<div class="max-w-3xl mx-auto px-4 py-4">
<a href="/" class="text-xl font-bold">{{ config('app.name') }}</a>
</div>
</header>
<main>
@yield('content')
</main>
<footer class="border-t mt-16 py-8 text-center text-sm text-gray-500">
© {{ date('Y') }} {{ config('app.name') }}
</footer>
</body>
</html>
Then point the config at it:
'layouts' => [
'public' => 'layouts.blog-public',
'admin' => 'blog::layouts.admin',
],
If your layout already loads its own CSS via @vite, set 'assets' => [] in config/blog.php to avoid loading it twice.
Public layout: reuse a Blade component (slot-based)
If your app already has a slot-based layout (Breeze, Jetstream, Folio, or a custom <x-site-layout>), create a small wrapper that bridges @yield('content') and {{ $slot }}:
{{-- resources/views/layouts/blog-public.blade.php --}}
<x-site-layout>
@yield('content')
</x-site-layout>
'public' => 'layouts.blog-public',
Three lines of plumbing, your app's chrome wrapping the blog.
Admin layout
The admin Livewire components use ->layout() (slot-based), so the admin layout key can point at any slot-based view directly:
'admin' => 'layouts.app', // your <x-app-layout> view, no wrapper needed
If your admin layout is @yield-based instead, build a small slot bridge:
{{-- resources/views/layouts/blog-admin.blade.php --}}
<!DOCTYPE html>
<html>
<head>
@vite('resources/css/app.css')
@livewireStyles
</head>
<body>
{{ $slot }}
@livewireScripts
</body>
</html>
Required directives in your layout
The package emits SEO meta tags, OG tags and JSON-LD via @push('head'). For these to render, your layout's <head> must contain:
@stack('head')
Without it, <title> and meta tags from posts won't appear in the head.
Customizing the visual style
The package ships with emerald as the accent colour and zinc as the neutral. To brand it differently, publish the views and edit them directly. Three granularities are available:
# Everything (public + admin + layouts + icons)
php artisan vendor:publish --tag="simple-blog-views"
# Just the public side (index, show, public layout)
php artisan vendor:publish --tag="simple-blog-views-public"
# Just the admin side (Livewire admin views + admin layout)
php artisan vendor:publish --tag="simple-blog-views-admin"
# Just the layouts (public and admin shells, no inner content)
php artisan vendor:publish --tag="simple-blog-views-layouts"
The Blade files land in resources/views/vendor/blog/. From that point Laravel uses your published copies, so a global find-and-replace (emerald- → blue-, emerald- → rose-, etc.) is enough to retheme that slice.
Trade-off: published views are frozen at the version you published — bug fixes and new features that ship in later releases of the package won't reach them automatically. Publishing only the slice you actually want to customize (admin or public) keeps the rest tracking upstream.
Injecting a CTA into posts
Create a view (e.g. resources/views/components/blog-cta.blade.php) and point to it:
'cta_view' => 'components.blog-cta',
The view receives the $post variable and is rendered after the post content (on show) and below the feed (on index).
Models
The package exposes:
Jessecruz\SimpleBlog\Models\PostJessecruz\SimpleBlog\Models\PostCategory
Use them directly when needed (e.g. to generate a sitemap, export content):
use Jessecruz\SimpleBlog\Models\Post;
$posts = Post::published()->with('category')->latest('published_at')->get();
Testing
composer test
Changelog
See CHANGELOG.
License
MIT — see License File.