PHPackages                             ozankurt/modules-blog - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. ozankurt/modules-blog

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

ozankurt/modules-blog
=====================

Headless blog module for Laravel with scheduled publishing, SEO, translations, and Filament admin.

v2.2.1(2mo ago)884MITPHPPHP ^8.4CI passing

Since Jun 19Pushed 3w agoCompare

[ Source](https://github.com/OzanKurt/KurtModules-Blog)[ Packagist](https://packagist.org/packages/ozankurt/modules-blog)[ RSS](/packages/ozankurt-modules-blog/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (55)Versions (21)Used By (0)

laravel-modules-blog
====================

[](#laravel-modules-blog)

Headless blog module for Laravel: posts, categories, tags, comments, scheduled publishing, SEO meta, translatable content, Spatie medialibrary.

Requirements
------------

[](#requirements)

- PHP 8.4+
- Laravel 12.x or 13.x
- `ozankurt/laravel-modules-core` v2.2+ (ships the API kit)

Installation
------------

[](#installation)

```
composer require ozankurt/laravel-modules-blog
```

Publish config and migrations:

```
php artisan vendor:publish --tag=blog-config
php artisan vendor:publish --tag=blog-migrations
php artisan migrate
```

What it provides
----------------

[](#what-it-provides)

- `Kurt\Modules\Blog\Models\Post` — with translatable title/excerpt/body/meta\_\*, status enum (Draft/Scheduled/Published/Archived), type enum (Text/Image/Video/Carousel), scopes (`published`, `scheduled`, `drafts`, `popular`, `inCategory`, `withTags`, `authoredBy`), a `relatedTo` scope, and a `related()` helper.
- `Category`, `Tag`, `Comment` models with their relations and scopes.
- `BlogAuthor` contract + `IsBlogAuthor` trait for your User model.
- `Kurt\Modules\Blog\Support\VideoUrl::parse()` for YouTube / Vimeo / DailyMotion URL parsing.
- `Kurt\Modules\Blog\Support\SeoMetadata::forPost()` for SEO meta resolution.
- `Kurt\Modules\Blog\Support\FeedBuilder` for RSS 2.0 / feed data, and `SitemapBuilder` for sitemap entries — see [Headless helpers](#headless-helpers).
- An opt-in JSON REST API (posts, categories, tags, comments + `publish`/`unpublish`/`related` actions) — see [API](#api).
- Console commands: `blog:publish-due`, `blog:upgrade-translations`, `blog:demo`.
- Domain events: `PostCreated`, `PostUpdated`, `PostPublished`, `PostArchived`, `CommentCreated`, `CommentApproved`, `CommentRejected`, ...

Related posts
-------------

[](#related-posts)

`$post->related(int $limit = 5)` returns published, non-self posts ranked by relatedness: those sharing the most tags come first, then a shared category acts as a fallback so lightly-tagged posts still surface neighbours. It runs as a single query (no N+1) — only the current post's tag ids are loaded up front, and the overlap count is a correlated subquery over the pivot.

```
$related = $post->related();      // Collection, up to 5
$related = $post->related(10);    // widen the limit

// Or compose from the underlying scope (adds `shared_tags` /
// `shared_category` ordering columns), e.g. with eager loads:
Post::relatedTo($post)->with('category')->limit(3)->get();
```

A post with neither tags nor a category has no neighbours and yields an empty collection.

Headless helpers
----------------

[](#headless-helpers)

The module ships no routes or views. Feed and sitemap generation are provided as support classes that return a string or a data structure; your app decides where to expose them.

### RSS / feed — `FeedBuilder`

[](#rss--feed--feedbuilder)

`Kurt\Modules\Blog\Support\FeedBuilder` builds the latest published posts into an RSS 2.0 XML string (`toRss()`) or a plain data structure (`toArray()`, for JSON Feed / Atom / a Blade view). Count is configurable, and the feed can be scoped to a category. All dynamic text is XML-escaped, so `toRss()` is always well-formed.

```
use Kurt\Modules\Blog\Support\FeedBuilder;

// In the consuming app's routes:
Route::get('feed', fn () => response(
    FeedBuilder::make()->limit(20)->toRss(),
    200,
    ['Content-Type' => 'application/rss+xml; charset=UTF-8'],
));

// Per-category feed, custom item URLs and channel metadata:
FeedBuilder::make()
    ->forCategory($category)
    ->title('My Blog')
    ->link(url('/'))
    ->linkUsing(fn ($post) => route('posts.show', $post->slug))
    ->toRss();
```

Defaults (feed title/description/limit) live under the `feed` key in `config/blog.php`.

### Sitemap — `SitemapBuilder`

[](#sitemap--sitemapbuilder)

`Kurt\Modules\Blog\Support\SitemapBuilder` returns `SitemapEntry` objects (`loc` / `lastmod` / `changefreq` / `priority`) for public content: every published post, every category holding at least one published post, and (opt-in) every tag that does. Draft, scheduled and future-dated posts — and categories/tags whose only posts are non-public — are excluded.

```
use Kurt\Modules\Blog\Support\SitemapBuilder;

$entries = SitemapBuilder::make()
    ->includeTags()                                   // optional
    ->postLinkUsing(fn ($post) => route('posts.show', $post->slug))
    ->entries();                                      // Collection

$rows = SitemapBuilder::make()->toArray();            // array of loc/lastmod/... rows
```

Feed the entries into whichever sitemap package or response your app already uses. Per-type change frequencies live under the `sitemap` key in `config/blog.php`.

API
---

[](#api)

The module ships an out-of-the-box JSON REST API built on the Core API kit (`ozankurt/laravel-modules-core` v2.2+). It is **safe by default**: nothing is registered until you opt in.

### Enabling

[](#enabling)

Set the mode to `api` (or `ui`) — headless registers no routes:

```
BLOG_HTTP_MODE=api
```

Everything is driven by the `http` block published to `config/blog.php`:

```
'http' => [
    'mode' => env('BLOG_HTTP_MODE', 'headless'), // headless | api | ui
    'prefix' => 'api/blog',                       // URL prefix for every route
    'middleware' => ['api'],                      // base middleware (all routes)
    'auth_middleware' => ['auth'],                // added to write routes; e.g. ['auth:sanctum']
    'rate_limit' => '60,1',                        // maxAttempts,decayMinutes for throttle:blog-api
],
```

Every route is throttled by the named `blog-api` limiter (keyed by user id, or client IP for guests).

### Endpoints

[](#endpoints)

All paths are relative to the configured prefix (default `/api/blog`). Responses use the Core `{ "data": ..., "meta": ... }` envelope; index endpoints add `meta.pagination`.

MethodPathAuthDescriptionGET`/posts`publicList posts. `?sort=created_at,-published_at,title`, `?filter[category]=`, `?filter[status]=`, `?filter[author]=`, `?per_page=`.GET`/posts/{id|slug}`publicShow a post by id or slug.POST`/posts`authCreate a post (authored by the current user).PATCH/PUT`/posts/{id|slug}`authUpdate a post.DELETE`/posts/{id|slug}`authSoft-delete a post (204).POST`/posts/{id|slug}/publish`authPublish now (backfills `published_at`).POST`/posts/{id|slug}/unpublish`authRevert to draft.GET`/posts/{id|slug}/related`publicRelated posts (shared tags, then category). `?limit=`.GET`/posts/{id|slug}/comments`publicApproved comments for a post (staff see all).POST`/posts/{id|slug}/comments`authAdd a comment to a post (201).PATCH/PUT`/comments/{id}`authEdit a comment.DELETE`/comments/{id}`authSoft-delete a comment (204).GET`/categories`publicList categories.GET`/categories/{id}`publicShow a category.POST`/categories`authCreate a category.PATCH/PUT`/categories/{id}`authUpdate a category.DELETE`/categories/{id}`authSoft-delete a category (204).GET`/tags`publicList tags.GET`/tags/{id}`publicShow a tag.POST`/tags`authCreate a tag.DELETE`/tags/{id}`authSoft-delete a tag (204).### Auth &amp; policies

[](#auth--policies)

- **Reads are public** and respect the published scope: guests and non-staff readers only see published posts (an authenticated reader also sees their own drafts; staff see everything). Requesting a draft you may not view returns 403.
- **Writes require authentication** (the `auth_middleware`) and are additionally guarded by the module's Policies (`PostPolicy`, `CommentPolicy`, `CategoryPolicy`, `TagPolicy`) via `$this->authorize()` in every write action. Post/comment writes allow the owner or staff; category/tag writes are staff-only. "Staff" is whatever your app grants through the `canManageBlog`gate — define it in your `AuthServiceProvider`:

    ```
    Gate::define('canManageBlog', fn ($user) => $user->is_admin);
    ```

Requests are validated with FormRequests, so invalid payloads return the standard `422` `{ "message": ..., "errors": ... }` envelope.

Filament admin
--------------

[](#filament-admin)

The package ships parallel admin resource sets for Filament **v3, v4, and v5** — `PostResource`, `CategoryResource`, `TagResource`, and `CommentResource`. The correct set is chosen at runtime from the installed Filament major, so you register a single version-dispatching plugin on your panel:

```
use Filament\Panel;
use Kurt\Modules\Blog\Filament\BlogPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(BlogPlugin::make());
}
```

`BlogPlugin::make()` resolves to the matching `V3`/`V4`/`V5` plugin via `Kurt\Modules\Core\Support\FilamentVersion`. Install whichever Filament major your app uses — the resources require nothing beyond what the module already depends on:

```
# whichever your app runs
composer require filament/filament:"^3.0|^4.0|^5.0"
composer require filament/spatie-laravel-media-library-plugin:"^3.0|^4.0|^5.0"
```

What the resources give you:

- **Posts** — per-locale (en/tr) translatable title/excerpt/body and SEO meta; status and type enum selects; a `scheduled_for` picker shown when the status is *Scheduled* and a `video_url` field shown when the type is *Video*; category and tag relationship selects; a Spatie media-library cover upload; a table with status/type filters, badges, author, category, publish date and view count.
- **Categories** — translatable name/description, parent (tree) select, slug read-only on edit, post counts.
- **Tags** — translatable name/description with a colour picker and a colour swatch column.
- **Comments** — a moderation queue defaulting to pending, with approve/reject row actions and approve/reject/delete bulk actions.

License
-------

[](#license)

MIT (c) Ozan Kurt

###  Health Score

55

—

FairBetter than 97% of packages

Maintenance91

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity90

Battle-tested with a long release history

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~330 days

Recently: every ~0 days

Total

12

Last Release

75d ago

Major Versions

0.4.0 → v2.0.x-dev2026-05-28

### Community

Maintainers

![](https://www.gravatar.com/avatar/c24006c03ba24fcb1c65905a009600b34e17d532add4a381075a2d7379cf4b94?d=identicon)[OzanKurt](/maintainers/OzanKurt)

---

Top Contributors

[![OzanKurt](https://avatars.githubusercontent.com/u/8682003?v=4)](https://github.com/OzanKurt "OzanKurt (141 commits)")

---

Tags

laravelblogfilamentkurtmodules

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/ozankurt-modules-blog/health.svg)

```
[![Health](https://phpackages.com/badges/ozankurt-modules-blog/health.svg)](https://phpackages.com/packages/ozankurt-modules-blog)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[spatie/laravel-health

Monitor the health of a Laravel application

88212.7M185](/packages/spatie-laravel-health)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M306](/packages/laravel-ai)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5226.7k](/packages/simplestats-io-laravel-client)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[masterix21/laravel-licensing

Laravel licensing package with polymorphic assignment to any model, activation keys, expirations/renewals, and seat control via LicenseUsage. Supports offline verification with public-key–signed tokens, a CLI to generate/rotate/revoke keys, and an extensible architecture via config and contracts.

1614.1k4](/packages/masterix21-laravel-licensing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
