Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Simple Blog Laravel Package

ojessecruz/simple-blog

A ready-to-use Laravel blog package: a clean, reading-focused public listing plus Livewire admin CRUD, Markdown with HTML escaping, and draft previews in a new tab. Auth is left to your own middleware. Install with one command; bring Tailwind.

View on GitHub
Deep Wiki
Context7

Getting Started

Install in three commands. The package ships an idempotent installer that does the boring parts — config, migrations, and Tailwind wiring:

composer require ojessecruz/simple-blog
php artisan simple-blog:install   # publishes config + migrations, injects the Tailwind @import, offers to migrate
npm run build

Re-running simple-blog:install is safe; it won't duplicate the CSS import. If resources/css/app.css doesn't exist, it prints the line to paste manually.

Look here first. After install, open config/blog.php. It is the entire surface area of the package — every behavior you'll care about day-to-day is a key in that file, and each key is commented. Then open php artisan route:list --path=blog to see the eight routes you just inherited.

First real use case: lock the door. The package deliberately ships no authorization. Before you visit /admin/blog in a browser, set admin_middleware:

// config/blog.php
'admin_middleware' => ['web', 'auth', 'can:manage-blog'],
'public_middleware' => ['web'],
'author_model'      => App\Models\User::class,
// AuthServiceProvider (or bootstrap/app.php with a closure)
Gate::define('manage-blog', fn ($user) => $user->is_admin === true);

Second step: make User an Author. The admin panel renders author bylines and avatars, and it talks to your model through a contract rather than assuming column names:

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)) ?: [];
        return implode('', array_map(
            fn ($w) => mb_strtoupper(mb_substr($w, 0, 1)),
            array_slice($words, 0, 2),
        ));
    }

    public function getBlogAuthorAvatarUrl(): ?string { return $this->avatar_url; }
}

Then write one post in the admin, publish it, and hit /blog and /blog/{slug}. That is the whole happy path — the rest of the package is customization.

Verify before you build on it: confirm the published migrations created posts and post_categories tables, confirm /blog renders styled (not just unstyled markup — see Gotchas), and confirm a draft is invisible on /blog but visible at /admin/blog/{slug}/preview.


Implementation Patterns

Wire the blog into your app's chrome, not the other way around

The default public layout is deliberately plain. The fastest integration is a three-line bridge around your existing slot-based layout:

{{-- resources/views/layouts/blog-public.blade.php --}}
<x-site-layout>
    @yield('content')
</x-site-layout>
'layouts' => [
    'public' => 'layouts.blog-public',
    'admin'  => 'layouts.app',   // admin Livewire components use ->layout(), so slot-based works directly
],

For the public side, if you want full control (SEO meta, header, footer), write a @yield-based layout from scratch instead — the package only requires @yield('content') and @stack('head').

Kill the double CSS load

If your layout already calls @vite('resources/css/app.css') and the package also emits asset tags, you get the stylesheet twice. Set:

'assets' => [],

This is the single most common integration mistake after Tailwind wiring.

Pick a middleware strategy and stay consistent

Three patterns work; pick one per app:

// Gate-based — best when "who can blog" is a permission, not a role
'admin_middleware' => ['web', 'auth', 'can:manage-blog'],

// Separate guard — best when admins are a distinct model/table
'admin_middleware' => ['web', 'auth:admin'],

// Stack — best when your own middleware already encodes the policy
'admin_middleware' => ['web', 'auth', 'verified', 'super.admin'],

The can: form requires the Gate to exist before the route is resolved; define it in a service provider, not inline in a controller.

Tailwind: two wirings, pick by version

Tailwind v4 is a one-line import in resources/css/app.css (the installer does it for you):

@import 'tailwindcss';
@import '../../vendor/ojessecruz/simple-blog/resources/css/simple-blog.css';

The path resolves relative to the package file, so your directory structure can change without breaking it. Tailwind v3 needs the views in content:

content: [/* ... */, './vendor/ojessecruz/simple-blog/resources/views/**/*.blade.php'],

Retheme by slice, not wholesale

Publishing everything with --tag="simple-blog-views" freezes public, admin, layouts, and icons at today's version. Publish only the slice you intend to change:

php artisan vendor:publish --tag="simple-blog-views-public"   # index, show, public layout
php artisan vendor:publish --tag="simple-blog-views-admin"    # Livewire views + admin layout
php artisan vendor:publish --tag="simple-blog-views-layouts"  # shells only

Published files land in resources/views/vendor/blog/ and shadow the package copies. A find-and-replace of emerald- → your accent is the intended retheme path.

Draft → preview → publish

The editorial workflow is: save a post with a draft status, click preview (opens /admin/blog/{slug}/preview in a new tab), iterate, flip to published. The preview route lives behind admin_middleware, so drafts never leak publicly — but that also means the preview URL is useless to anyone who isn't an authorized admin. Don't hand it to a ghostwriter expecting it to work without a login.

Use the models directly for anything the UI doesn't cover

The package exposes Jessecruz\SimpleBlog\Models\Post and PostCategory. This is your escape hatch for sitemaps, RSS, exports, and dashboard widgets:

use Jessecruz\SimpleBlog\Models\Post;

$posts = Post::published()->with('category')->latest('published_at')->get();

A sitemap entry is roughly a route('blog.show', $post->slug) per row; RSS is the same loop with a Content-Type header.

Inject a CTA without touching package views

cta_view renders after post content on the show page and below the feed on the index. Point it at a component:

'cta_view' => 'components.blog-cta',

The view receives $post on the show page. On the index it's rendered feed-wide — treat $post as possibly absent there and guard accordingly.

Route prefixes as collision insurance

If your app already owns /blog (a marketing page, a redirect, an existing CMS), don't fight route order — remount:

'route_prefix'       => 'articles',
'admin_route_prefix' => 'admin/articles',

The route names (blog.index, blog.show, blog.admin.index) stay stable, so route('blog.show', $post) keeps working after a prefix change. Always reference names, never hardcoded paths.

Testing against it

There are no shipped factories, so seed with the model:

Post::create([
    'title' => 'Hello',
    'slug' => 'hello',
    'body' => "# Heading\n\nSome **markdown**.",
    'published_at' => now(),
    // author relation / status per your schema
]);

Then feature-test the public routes with RefreshDatabase and assert that an unpublished post returns 404 on blog.show while an authenticated admin gets 200 on blog.admin.preview.


Gotchas and Tips

Unstyled blog = Tailwind never scanned the package. This is the #1 failure. The package ships Tailwind classes but compiles nothing. If /blog renders as bare HTML with no spacing, your @import (v4) or content entry (v3) is missing, or you forgot npm run build. Running simple-blog:install again is the fastest fix.

@stack('head') is mandatory for SEO. The package pushes <title>, OG tags, and JSON-LD via @push('head'). A custom layout without @stack('head') in <head> silently drops all of it — no error, just missing meta. Grep your layout for it before debugging anything SEO-related.

admin_middleware is unprotected by default. The package makes zero assumptions about auth, which means a fresh install has a world-writable admin panel. Protect it in the same deploy as composer require, not in a follow-up ticket.

The Author contract is enforced at runtime, not install time. Pointing author_model at a User that doesn't implement Jessecruz\SimpleBlog\Contracts\Author fails when the admin or public views try to render a byline. Implement all three methods (Name, Initials, AvatarUrl); return null from the avatar method rather than omitting it.

Markdown escapes HTML by design. Raw <iframe>, <video>, and <script> in post bodies will be escaped, not rendered. That's the intended safety default. If you need embeds, adjust the markdown config options passed to Str::markdown() — and accept that you're trading escaping for trust in your authors. Keep allow_unsafe_links off unless you audit authors.

Version-pin around the Livewire 4 / Laravel 11 fixes. The changelog is worth reading once: 0.5.0 fixed admin components being registered as blog::admin.* (which Livewire 4 can't resolve — it reserves ::) and replaced the #[Scope] attribute on published() with the conventional scopePublished() so Laravel 11 works. On Laravel 11, or on Livewire 4, require ^0.5 minimum. If you've written your own Blade that references admin components, use the dotted names (blog.admin.post-index) post-0.5.

Published views are frozen. Once you publish a slice, package bugfixes stop reaching it. When upgrading, git diff your resources/views/vendor/blog/ against the newly installed package views to see whether you're missing a fix — or publish only the slice you actually customized and let the rest track upstream.

Custom admin layouts must include Livewire directives. The default admin layout handles @livewireStyles / @livewireScripts. If you point layouts.admin at a hand-rolled @yield-based shell, include them explicitly or the admin panel renders inert.

Config changes need a clear. Every key here is read through config(). If you use php artisan config:cache in deploy, re-cache after editing config/blog.php — otherwise you'll debug a middleware change that "did nothing."

CTA $post on the index. The same cta_view renders on both the show page and below the feed. isset($post) / $post ?? null in the CTA view prevents an undefined-variable blow-up on the listing page.

Small ecosystem, so own your extension points. At 1 star and a 0.x series, expect the package to be a solid, narrow CRUD core rather than a platform. Don't fork the vendor directory or edit published views to add features — add them around the package: your own routes that query Post directly, your own Blade layout, your own CTA view. That way composer update stays a non-event.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource