marko / blog
Blog module for Marko Framework
Requires
- php: ^8.5
- marko/cache: 0.0.1
- marko/config: 0.0.1
- marko/core: 0.0.1
- marko/database: 0.0.1
- marko/mail: 0.0.1
- marko/routing: 0.0.1
- marko/session: 0.0.1
- marko/view: 0.0.1
Requires (Dev)
- marko/testing: 0.0.1
- pestphp/pest: ^4.0
Suggests
- marko/admin: Admin panel integration for managing blog content
- marko/admin-auth: Admin authentication for blog admin controllers
- marko/mail-log: Log-based mail driver for development and debugging
- marko/mail-smtp: SMTP mail driver for production email sending
- marko/view-latte: Latte template engine for default blog templates
This package is auto-updated.
Last update: 2026-03-25 21:07:48 UTC
README
WordPress-like blog for Marko — posts, authors, categories, tags, and threaded comments with email verification.
Installation
composer require marko/blog
Required: A view driver (e.g., marko/view-latte) and a database driver (e.g., marko/database-mysql):
composer require marko/blog marko/view-latte marko/database-mysql
Quick Example
Once installed with a view and database driver, the blog works automatically:
- Run migrations to create tables:
marko db:migrate - Visit
/blogto see the post list - Visit
/blog/{slug}to view a single post
View Templates
The blog package ships with Latte templates (via marko/view-latte) but supports any template engine.
Overriding Templates
You can override any blog template in your app module by creating a matching path under app/views/blog/. The app module's view path takes precedence, allowing full control over rendering without modifying the package.
For example, to override the post list template:
app/views/blog/post/list.latte
Alternative View Engines
To use a different template engine such as Blade or Twig, install the corresponding view driver package and register your own view implementations via Preferences. Any view driver implementing the view contract works.
Configuration
Publish or set the following keys in your config:
| Key | Default | Description |
|---|---|---|
posts_per_page |
10 |
Number of posts shown per page |
comment_max_depth |
3 |
Maximum thread depth for nested comments |
comment_rate_limit_seconds |
60 |
Minimum seconds between comments from one user |
verification_token_expiry_days |
7 |
Days before a comment verification token expires |
route_prefix |
/blog |
URL prefix for all blog routes |
Extensibility
Preferences (Swap Implementations)
Use the #[Preference] attribute to swap any blog class with your own implementation. This lets you replace or override core behavior without forking:
use Marko\Core\Attributes\Preference; #[Preference(PostRepositoryInterface::class)] class MyPostRepository implements PostRepositoryInterface { // custom implementation }
Plugins (Hook Methods)
Use #[Plugin] with #[Before] and #[After] attributes to hook into any public method without overriding the entire class:
use Marko\Core\Attributes\Plugin; use Marko\Core\Attributes\Before; use Marko\Core\Attributes\After; #[Plugin(target: PostService::class)] class PostServicePlugin { /** * The method name matches the target method. #[Before] determines timing. * Return null to pass through, an array to modify arguments, or a non-null * non-array value to short-circuit and skip the original method. */ #[Before] public function createPost(array $data): null|array { // Modify the data before createPost runs by returning an array $data['slug'] = strtolower(trim($data['slug'])); return [$data]; } } #[Plugin(target: PostService::class)] class PostServiceAuditPlugin { #[After] public function createPost(Post $post): Post { // Act on the result after createPost runs return $post; } }
Observers (React to Events)
Use #[Observer] to react to blog lifecycle events without modifying core classes:
use Marko\Core\Attributes\Observer; use Marko\Blog\Events\Post\PostCreated; #[Observer(event: PostCreated::class)] class PostCreatedObserver { public function handle(PostCreated $event): void { // react to event — send notification, update cache, etc. } }
Observers are ideal for event-driven side effects such as sending emails, clearing caches, or syncing to external services. Use an observer for every event reaction you need to add.
Available Events
The blog dispatches the following lifecycle events:
| Event | Trigger |
|---|---|
PostCreated |
A new post is created |
PostUpdated |
A post is updated |
PostPublished |
A post is published |
PostDeleted |
A post is deleted |
CommentCreated |
A new comment is submitted |
CommentVerified |
A comment email is verified |
CommentDeleted |
A comment is deleted |
CategoryCreated |
A new category is created |
TagCreated |
A new tag is created |
AuthorCreated |
A new author is created |
Routes
The blog registers the following public routes (prefix configurable via route_prefix):
| Method | Path | Description |
|---|---|---|
GET /blog |
Post list | Paginated list of published posts |
GET /blog/{slug} |
Post detail | Single post view |
GET /blog/category/{slug} |
Category archive | Posts filtered by category |
GET /blog/tag/{slug} |
Tag archive | Posts filtered by tag |
GET /blog/author/{slug} |
Author archive | Posts filtered by author |
GET /blog/search |
Search results | Full-text post search |
POST /blog/{slug}/comment |
Submit comment | Submit a comment on a post |
GET /blog/comment/verify/{token} |
Verify comment | Verify a comment via email token |
CLI Commands
| Command | Description |
|---|---|
blog:publish-scheduled |
Publish any posts whose scheduled publish date has passed |
blog:cleanup |
Remove expired verification tokens and other stale data |
Documentation
Full usage, configuration, events, API reference, and examples: marko/blog