PHPackages                             jcfrane/laravel-md-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. [Parsing &amp; Serialization](/categories/parsing)
4. /
5. jcfrane/laravel-md-blog

ActiveLibrary[Parsing &amp; Serialization](/categories/parsing)

jcfrane/laravel-md-blog
=======================

Write blog posts as markdown files with YAML front matter. No database, no CMS — just files and a clean query API.

v1.1.0(2mo ago)3120↓75%[1 issues](https://github.com/jcfrane/laravel-md-blog/issues)MITPHPPHP ^8.2

Since Feb 28Pushed 2mo agoCompare

[ Source](https://github.com/jcfrane/laravel-md-blog)[ Packagist](https://packagist.org/packages/jcfrane/laravel-md-blog)[ RSS](/packages/jcfrane-laravel-md-blog/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (5)Dependencies (7)Versions (7)Used By (0)

laravel-md-blog
===============

[](#laravel-md-blog)

Write blog posts as markdown files with YAML front matter. No database, no CMS — just files and a clean query API.

Built for Laravel 12+ / PHP 8.2+.

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

[](#installation)

```
composer require jcfrane/laravel-md-blog
```

Publish the config (optional):

```
php artisan vendor:publish --tag=md-blog-config
```

Usage
-----

[](#usage)

### Writing Posts

[](#writing-posts)

Create `.md` files in `resources/markdown/blog/` (configurable) with YAML front matter:

```
---
title: My First Post
slug: my-first-post
date: 2026-02-28
tags: [laravel, php]
category: tutorials
excerpt: A quick intro to my blog.
published: true
---

# My First Post

Your markdown content here.
```

**Front matter fields:**

FieldRequiredDefault`title`NoFilename`slug`NoFilename`date`NoFile modification time`tags`No`[]``category`No`""``excerpt`No`""``published`No`true`Any additional front matter fields are available via `$post->meta`.

### Querying Posts

[](#querying-posts)

```
use JCFrane\MdBlog\Facades\MdBlog;

// All published posts
$posts = MdBlog::all();

// Single post by slug
$post = MdBlog::find('my-first-post');

// Filter by tag
$posts = MdBlog::whereTag('laravel');

// Filter by category
$posts = MdBlog::whereCategory('tutorials');

// Published posts, newest first
$posts = MdBlog::latest();

// Clear the cache
MdBlog::clearCache();
```

All query methods return Laravel Collections (except `find()` which returns a `Post` or `null`). Drafts (`published: false`) are automatically excluded.

### The Post Object

[](#the-post-object)

Each post is an immutable `Post` DTO with these properties:

```
$post->title;        // string
$post->slug;         // string
$post->date;         // Carbon instance
$post->body;         // raw markdown
$post->html;         // rendered HTML
$post->tags;         // array
$post->category;     // string
$post->excerpt;      // string
$post->published;    // bool
$post->meta;         // array (extra front matter fields)
$post->filePath;     // string
$post->lastModified; // int (unix timestamp)
```

`Post` implements `Arrayable`, `Jsonable`, and `JsonSerializable` for clean Inertia/API integration:

```
// In an Inertia controller
return Inertia::render('Blog/Show', [
    'post' => MdBlog::find($slug),
]);
```

### Images

[](#images)

The package automatically handles images stored within your blog directory.

#### Relative Images

[](#relative-images)

If you have an image at `resources/markdown/blog/images/photo.png`, you can reference it in your markdown as:

```
![My Photo](images/photo.png)
```

The package will automatically rewrite this URL to a dedicated route that serves the image securely.

#### Image Configuration

[](#image-configuration)

By default, images are served with a long cache duration (24 hours). You can customize this in the config:

```
'images' => [
    'enabled'   => true,
    'cache_ttl' => 86400, // in seconds
],
```

When `images.enabled` is `true` (default), the package registers a route at `/{route_prefix}/images/{path}` to serve your assets.

### Email

[](#email)

Send blog posts as emails to your subscribers. The feature is opt-in and disabled by default.

#### Setup

[](#setup)

1. Set the environment variables:

```
MD_BLOG_MAIL_ENABLED=true
MD_BLOG_MAIL_RECIPIENT_MODEL=App\Models\User
```

2. Implement the `EmailRecipient` interface on your model:

```
use JCFrane\MdBlog\Contracts\EmailRecipient;
use JCFrane\MdBlog\Traits\ReceivesPostMail;
use JCFrane\MdBlog\Post;

class User extends Authenticatable implements EmailRecipient
{
    use ReceivesPostMail;

    public function shouldReceivePostEmail(Post $post): bool
    {
        return $this->subscribed_to_newsletter;
    }
}
```

The `ReceivesPostMail` trait provides defaults for `emailRecipients()` (returns `static::cursor()`), `getEmailAddress()` (`$this->email`), and `getEmailName()` (`$this->name`). Override any of these if your model differs.

#### Sending

[](#sending)

Three ways to send:

```
# Artisan command — single post or entire directory
php artisan md-blog:send-mail resources/markdown/blog/my-post.md
php artisan md-blog:send-mail resources/markdown/blog/
```

```
// Facade
MdBlog::sendPost('resources/markdown/blog/my-post.md');
```

```
# HTTP route (POST /{route_prefix}/send-mail, protected by auth middleware)
curl -X POST /md-blog/send-mail -d '{"path": "resources/markdown/blog/my-post.md"}'
```

#### Queue Support

[](#queue-support)

Enable queued sending to avoid blocking:

```
MD_BLOG_MAIL_QUEUE=true
MD_BLOG_MAIL_QUEUE_CONNECTION=redis
MD_BLOG_MAIL_QUEUE_NAME=emails
```

#### Customizing the Email Template

[](#customizing-the-email-template)

Publish the views to customize the email layout:

```
php artisan vendor:publish --tag=md-blog-views
```

This copies the template to `resources/views/vendor/md-blog/mail/post.blade.php`.

Configuration
-------------

[](#configuration)

```
// config/md-blog.php
return [
    'path' => env('MD_BLOG_PATH', 'resources/markdown/blog'),

    'cache' => [
        'enabled' => env('MD_BLOG_CACHE_ENABLED', true),
        'store'   => env('MD_BLOG_CACHE_STORE', null), // null = app default
        'ttl'     => env('MD_BLOG_CACHE_TTL', 3600),
    ],

    'images' => [
        'enabled'   => env('MD_BLOG_IMAGES_ENABLED', true),
        'cache_ttl' => env('MD_BLOG_IMAGES_CACHE_TTL', 86400),
    ],

    'commonmark' => [
        'html_input'         => 'strip',
        'allow_unsafe_links' => false,
    ],

    'mail' => [
        'enabled'          => env('MD_BLOG_MAIL_ENABLED', false),
        'recipient_model'  => env('MD_BLOG_MAIL_RECIPIENT_MODEL', null),
        'queue'            => env('MD_BLOG_MAIL_QUEUE', false),
        'queue_connection' => env('MD_BLOG_MAIL_QUEUE_CONNECTION', null),
        'queue_name'       => env('MD_BLOG_MAIL_QUEUE_NAME', null),
        'chunk_size'       => env('MD_BLOG_MAIL_CHUNK_SIZE', 50),
        'middleware'       => ['auth'],
        'subject_prefix'   => env('MD_BLOG_MAIL_SUBJECT_PREFIX', ''),
    ],
];
```

Caching
-------

[](#caching)

Posts are cached per-file with automatic staleness detection — when a file is modified, the cached entry is automatically refreshed on the next read. Use `MdBlog::clearCache()` to force a full cache bust.

Development
-----------

[](#development)

### Running Tests

[](#running-tests)

```
composer install
./vendor/bin/phpunit
```

### Workbench (Demo App)

[](#workbench-demo-app)

The package includes a workbench powered by Orchestra Testbench:

```
php vendor/bin/testbench serve
```

Visit `http://localhost:8000/blog` to see the demo blog.

License
-------

[](#license)

MIT

###  Health Score

41

—

FairBetter than 89% of packages

Maintenance81

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

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 ~2 days

Total

6

Last Release

62d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/2dfbe5a9ae770e600c736bec7beef2ad46eca3cc19b3cb0a8de9816b615b8c96?d=identicon)[jcfrane](/maintainers/jcfrane)

---

Tags

laravelmarkdowncmsinertiablog

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/jcfrane-laravel-md-blog/health.svg)

```
[![Health](https://phpackages.com/badges/jcfrane-laravel-md-blog/health.svg)](https://phpackages.com/packages/jcfrane-laravel-md-blog)
```

###  Alternatives

[dniccum/nova-documentation

A Laravel Nova tool that allows you to add markdown-based documentation to your administrator's dashboard.

37116.4k](/packages/dniccum-nova-documentation)[torchlight/torchlight-commonmark

A Commonmark extension for Torchlight, the syntax highlighting API.

29256.6k6](/packages/torchlight-torchlight-commonmark)[dmcbrn/laravel-email-database-log

A simple database logger for all outgoing emails sent by Laravel website - built from shvetsgroup/laravel-email-database-log.

1633.6k](/packages/dmcbrn-laravel-email-database-log)

PHPackages © 2026

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