## Getting Started
### Minimal Setup
1. **Installation**
```bash
composer require thea/markdown-blog
php artisan vendor:publish --provider="Thea\MarkdownBlog\MarkdownBlogServiceProvider" --tag="migrations"
php artisan migrate
config/markdown-blog.php) and optionally the views (--tag="views").Configure Storage
Define your Markdown directory in config/markdown-blog.php:
'storage' => [
'path' => storage_path('app/blog/posts'),
'filename_pattern' => 'Y-m-d-{slug}.md',
],
Create the directory and ensure it’s writable.
First Post
Create a Markdown file in the configured directory (e.g., 2024-04-08-first-post.md):
---
title: First Post
slug: first-post
excerpt: A minimal example.
date: 2024-04-08
---
# Hello, World!
Content here...
Run php artisan markdown-blog:refresh to parse the file into the database.
Basic Route
Add to routes/web.php:
use Thea\MarkdownBlog\Http\Controllers\PostController;
Route::get('/blog', [PostController::class, 'index']);
Route::get('/blog/{slug}', [PostController::class, 'show']);
Local Development
storage/app/blog/posts/.php artisan markdown-blog:refresh to sync changes to the DB.php artisan serve.Production Deployment
markdown-blog:refresh:
# Example GitHub Actions step
- run: php artisan markdown-blog:refresh --force
Frontend Integration
@foreach($posts as $post) in resources/views/blog/index.blade.php.PostController to return JSON:
public function apiIndex() {
return response()->json(Post::latest()->get());
}
$post->frontmatter (e.g., {{ $post->frontmatter['custom_field'] }}).tags, author) to Markdown files and extend the Post model to cast them:
protected $casts = [
'frontmatter' => array_merge(['title', 'slug'], ['tags' => 'array']),
];
php artisan markdown-blog:tag to generate tag routes (if supported in future updates).draft- (e.g., draft-2024-04-08-post.md) and filter in queries:
Post::where('slug', 'not like', 'draft-%')->get();
Post model:
use Laravel\Scout\Searchable;
class Post extends Model implements Searchable {
// ...
public function toSearchableArray() {
return [
'title' => $this->title,
'excerpt' => $this->excerpt,
'content' => $this->content,
];
}
}
storage/app/blog/media/ and reference them in Markdown:

Serve files via:
Route::get('/storage/blog/{path}', function ($path) {
return Storage::disk('local')->response("blog/{$path}");
});
spatie/laravel-sitemap to generate a sitemap for blog posts:
Sitemap::add(Post::latest()->get(), function (Post $post) {
return route('blog.show', $post->slug);
});
Frontmatter Parsing Issues
php artisan markdown-blog:validate.storage/logs/laravel.log) for Symfony\Component\Yaml\Exception\ParseException.File Permissions
markdown-blog:refresh fails with "Permission denied" on storage directory.chmod -R 775 storage/app/blog
chown -R www-data:www-data storage/app/blog # Adjust user as needed
Slug Conflicts
2024-04-08-post.md and 2024-04-09-post.md) overwrite each other.filename_pattern config to enforce uniqueness:
'filename_pattern' => 'Y-m-d-H-i-s-{slug}.md', // Includes timestamp
Missing Views
php artisan vendor:publish --tag="markdown-blog-views"
resources/views/vendor/markdown-blog/.Database Sync Delays
--force flag to overwrite existing records:
php artisan markdown-blog:refresh --force
Route::get('/debug-frontmatter', function () {
$post = Post::first();
return response()->json($post->frontmatter);
});
content field in the DB matches the rendered Markdown (use parsedown/parsedown for testing):
use Parsedown;
$parsedown = new Parsedown();
$html = $parsedown->text(file_get_contents(storage_path('app/blog/posts/2024-04-08-post.md')));
php artisan list | grep markdown-blog
Custom Parsers
Thea\MarkdownBlog\Services\MarkdownParser class to support custom frontmatter or content processing:
class CustomParser extends MarkdownParser {
public function parse($content) {
// Add logic here (e.g., custom shortcodes)
return parent::parse($content);
}
}
AppServiceProvider:
$this->app->bind(
Thea\MarkdownBlog\Contracts\MarkdownParser::class,
CustomParser::class
);
Event Listeners
PostSaved event (if implemented in future versions):
// Example (hypothetical)
Post::saved(function ($post) {
// Send notification, update search index, etc.
});
Middleware for Drafts
Route::get('/blog/{slug}', [PostController::class, 'show'])
->middleware('checkDraft');
public function handle($request, Closure $next) {
$post = Post::where('slug', $request->slug)->first();
if (str_starts_with($post->slug, 'draft-')) {
abort(404);
}
return $next($request);
}
Testing
MarkdownBlog facade or service container to mock posts in tests:
$this->app->instance(
Thea\MarkdownBlog\Contracts\PostRepository::class,
MockPostRepository::class
);
$parser = app(Thea\MarkdownBlog\Contracts\MarkdownParser::class);
$html = $parser->parse("# Test\n\nContent");
$this->assertString
How can I help you explore Laravel packages today?