Blog & News
Add a full-featured blog and news section to your Larapen site. Create posts with categories, tags, featured images, and a threaded comment system with moderation: all fully translatable.
Rich Post Editor
Create posts with translatable titles, slugs, content, excerpts, and SEO metadata. Attach featured images via the media library.
Categories & Tags
Organize content with hierarchical categories (using the unified categories table) and a flexible tagging system.
Threaded Comments
Nested comment replies with configurable depth, moderation queue, auto-approval for trusted commenters, and CAPTCHA support.
Multi-Language
All posts, categories, and tags support translations via Spatie Translatable. Localized front-end URLs with locale prefix.
Email Notifications
Admins are notified of new comments. Comment authors are notified when someone replies to their comment.
Reading Time & Views
Automatic reading time calculation (configurable WPM) and view count tracking for each post.
Use Cases
Company Blog
Publish company news, product updates, and industry insights. Organize posts by category (e.g. “Product Updates”, “Industry News”, “Tutorials”) and let visitors engage through comments.
Portfolio Blog
Complement your portfolio with behind-the-scenes articles, case studies, and project write-ups. Tag posts with relevant project names or technologies for easy cross-referencing.
Multi-Language Content Hub
Publish articles in multiple languages (English, French, etc.) with per-locale slugs and content. Each post can have fully independent translations managed through the admin panel.
News Section
Use the blog as a press/news section. Leverage the “published at” date for scheduling and the “draft/published” workflow for editorial control.
Requirements
- Larapen CMS v1.0.0 or later
- PHP 8.3+
- MySQL 8.0+ (required for
JSON_SEARCHin translatable slug lookups) - The core categories table must exist (blog categories use the unified
categoriestable withcategorizable_type = 'post')
Installation
Step 1: Place the Add-on
Copy or symlink the blog folder into your Larapen "extensions/addons" directory:
Step 2: Activate the Add-on
Go to Admin → Add-ons → Installed Add-ons and activate Blog & News.
Step 3: Run Migrations
This creates 4 tables: blog_posts, blog_post_tags, blog_post_tag (pivot),
and blog_comments. Blog categories use the existing core categories table.
Step 4: Set Permissions
The add-on registers 16 permissions (see Permissions). Assign them to admin roles via Admin → Users → Roles & Permissions.
Step 5: Configure
Navigate to Admin → Blog → Settings to configure posts per page, comment moderation rules, notifications, and reading time settings. See Settings.
Step 6: Run Vite Build (if using themes)
Required if new SCSS/JS files were added to theme directories for blog pages.
Configuration
The blog add-on ships with a configuration file at config/blog.php that defines default values.
All settings can be overridden from the admin panel (stored in the settings table, group blog).
| Setting | Description | Default |
|---|---|---|
blog_posts_per_page |
Number of posts displayed per page on the blog listing. | 10 |
blog_related_posts_count |
Number of related posts shown at the bottom of each post detail page. | 3 |
blog_words_per_minute |
Average reading speed used to calculate the “X min read” estimate. | 200 |
blog_comments_enabled |
Enable or disable the comment system globally. | true |
blog_comments_require_approval |
When enabled, guest comments must be approved by an admin before appearing. Authenticated user comments are auto-approved. | true |
blog_allow_guest_comments |
Allow non-logged-in visitors to leave comments (requires name and email). | false |
blog_comments_max_depth |
Maximum nesting level for threaded comment replies (1–5). | 2 |
blog_auto_approve_trusted_commenters |
Auto-approve comments from users who already have a previously approved comment (matched by email). | false |
blog_notify_admin_on_comment |
Send email notifications to all admin users when a new comment or reply is posted. | true |
blog_notify_author_on_reply |
Send email notifications to comment authors when someone replies to their comment. | true |
blog_captcha_enabled |
Require CAPTCHA verification when posting comments (requires a CAPTCHA provider to be configured in core settings). | false |
Config File Defaults
The config/blog.php file also includes featured image dimensions used when processing uploads:
| Key | Description | Default |
|---|---|---|
featured_images.width |
Featured image width (px) | 1200 |
featured_images.height |
Featured image height (px) | 630 |
featured_images.thumbnail_width |
Thumbnail width (px) | 400 |
featured_images.thumbnail_height |
Thumbnail height (px) | 250 |
Admin: Posts
The Posts page (Blog → All Posts) is the primary interface for managing blog content.
Posts List
A sortable, paginated table (20 per page) showing:
- Featured image thumbnail
- Title (translatable)
- Category
- Author
- Status (Draft / Published)
- View count
- Comments count
- Published at date
Filters & Search
The posts list supports three filter dimensions:
- Search: searches within post titles (across all translated locales via
JSON_SEARCH) - Status: filter by
draftorpublished - Category: filter by a specific category
Creating & Editing Posts
The post form includes the following fields, each supporting per-locale translations:
Content Fields (per locale)
| Field | Validation | Notes |
|---|---|---|
title |
Required (default locale), max 255 | Translatable. Used to auto-generate slug. |
slug |
Optional, max 255 | Translatable. Auto-generated from title if left empty. |
content |
Optional | Translatable. WYSIWYG editor content. |
excerpt |
Optional, max 500 | Translatable. Short summary for listing pages. |
meta_title |
Optional, max 70 | Translatable. SEO title tag. |
meta_description |
Optional, max 160 | Translatable. SEO meta description. |
Non-Translatable Fields
| Field | Validation | Notes |
|---|---|---|
category_id |
Optional, must exist in categories |
Blog category (from unified categories table) |
featured_image |
Optional, image file | Uploaded via core media service |
status |
Required, draft or published |
Uses the PageStatus enum |
published_at |
Optional, date | Auto-set to current time on first publish if empty |
tags |
Optional, array of tag IDs | Multi-select from existing tags |
Str::slug().
Admin: Categories
Blog categories (Blog → Categories) use the core unified categories table,
scoped by categorizable_type = 'post'. This means they share the same table structure as
portfolio and other add-on categories, but are isolated via a global scope on the PostCategory model.
Category Fields
| Field | Notes |
|---|---|
name |
Translatable. Required for default locale. |
slug |
Translatable. Auto-generated from name if empty. |
description |
Translatable. Optional. |
parent_id |
Nullable. Supports one level of nesting (parent → child). |
position |
Integer for manual ordering. |
is_active |
Boolean. Inactive categories are hidden from front-end. |
Admin: Tags
Tags (Blog → Tags) are lightweight labels that can be attached to any post.
Unlike categories, tags are flat (no hierarchy) and are stored in the blog_post_tags table.
Tag Fields
| Field | Notes |
|---|---|
name |
Translatable. The display name of the tag. |
slug |
Translatable. URL-friendly identifier. |
The tags list shows each tag with its associated post count. Tags are searchable by name and paginated (20 per page).
detach()), but the posts themselves are not affected.
Admin: Comments
The Comments page (Blog → Comments) provides a moderation interface for all blog comments across all posts.
Comments List
A paginated table (20 per page) showing:
- Author: user name (if authenticated) or guest name/email
- Content: comment text preview
- Post: the blog post the comment belongs to
- Status: Approved / Pending badge
- Date
A pending count badge is shown in the header to quickly identify items needing attention.
Moderation
Per-comment actions:
- View: see full comment content, replies, and post context
- Approve (
PATCH): marks the comment as approved - Delete: permanently removes the comment
Filter by Status
Use the status query parameter to filter:
?status=pending: show only pending (unapproved) comments?status=approved: show only approved comments
Bulk Actions
Select multiple comments using checkboxes and apply bulk actions:
- Approve: approve all selected comments at once
- Delete: delete all selected comments
Bulk actions are sent as POST admin/blog/comments/bulk with action and
comma-separated ids.
Admin: Settings
The settings page (Blog → Settings) is organized into four sections:
Posts Display
- Posts Per Page: number of posts on the listing page (1–100)
- Related Posts: number of related posts shown on post detail pages (0–12)
- Words Per Minute: reading speed for reading time calculation (100–500)
Comments
- Enable Comments: global toggle for the comment system
- Require Approval: whether guest comments need admin approval (authenticated users are always auto-approved)
- Guest Comments: allow non-logged-in visitors to comment
- Reply Depth: maximum nesting level for threaded replies (1–5)
- Auto-Approve Trusted: auto-approve comments from emails that already have an approved comment
Notifications
- Admin Notification: email admins when a new comment/reply is posted
- Reply Notification: email comment authors when someone replies to their comment
CAPTCHA Protection
- Enable CAPTCHA for Comments: require CAPTCHA verification when posting comments
Requires a CAPTCHA provider to be configured in the core settings. If no provider is configured, a warning is shown with a link to the configuration page.
Front-end: Blog Listing
The blog listing page (/{locale}/blog) displays published posts with pagination.
Main Content
- Post cards: each showing: featured image thumbnail, title, excerpt, category badge, author name, published date, reading time, and view count
- Pagination: configurable posts per page
Sidebar
- Categories: list of active categories with post counts
- Recent Posts: the 5 most recently published posts
- Popular Posts: the 5 most viewed posts
- Tags: all tags that have at least one post
Front-end: Post Detail
The post detail page (/{locale}/blog/{slug}) renders the full post content.
Content
- Header: title, category, author, published date, reading time, view count
- Featured image: full-width hero image (via polymorphic media relation)
- Content body: rendered HTML content
- Tags: tag badges linked to tag filter pages
- Post navigation: previous / next post links
- Related posts: posts sharing the same category or tags (configurable count)
- Comments section: threaded comments with reply form (see Comments)
View Count Tracking
Each time the post detail page is loaded, PostService::incrementViewCount()
increments the view_count column. This drives the “Popular Posts” sidebar widget.
Related Posts Algorithm
Related posts are selected by matching:
- Posts in the same category
- Posts sharing any of the same tags
Results are ordered by published date (most recent first) and limited to the configured count.
Front-end: Category & Tag Pages
Category Page
URL: /{locale}/blog/category/{slug}
Displays all published posts in the specified category with the same pagination, sidebar widgets, and post card layout as the main listing. The category is resolved by its translatable slug (current locale first, then English fallback).
Tag Page
URL: /{locale}/blog/tag/{slug}
Displays all published posts tagged with the specified tag. Same layout as the category page. The tag is resolved by its translatable slug.
Front-end: Search
URL: /{locale}/blog/search?q={query}
Full-text search across post titles and content in all locales using MySQL JSON_SEARCH.
Results are paginated and displayed with the standard blog listing layout.
Front-end: Comments
The comment system appears at the bottom of each post detail page (when enabled).
Comment Form
- Authenticated users: only need to enter the comment content. Auto-approved unless moderation overrides apply.
- Guest users (if enabled): must provide name, email, and content. Subject to approval moderation.
- Reply form: inline reply forms appear when clicking “Reply” on an existing comment, up to the configured max depth.
- Notify on reply: checkbox to opt in/out of reply notifications.
- CAPTCHA: displayed when CAPTCHA is enabled for blog comments.
Comment Display
- Comments are displayed in threaded format (parent → replies).
- Only approved comments are shown to front-end visitors.
- Each comment shows: author display name, date, content, and reply count.
Validation Rules
| Field | Validation |
|---|---|
content |
Required, 3–2000 characters |
parent_id |
Optional, must exist in blog_comments; depth check enforced |
author_name |
Required for guests, max 255 |
author_email |
Required for guests, valid email, max 255 |
notify_on_reply |
Optional boolean |
Auto-Approval Logic
- If Require Approval is off → all comments are auto-approved.
- If the commenter is authenticated → auto-approved.
- If Auto-Approve Trusted is on and the email has a previously approved comment → auto-approved.
- Otherwise → pending (requires admin approval).
Multi-Language Support
The blog add-on uses spatie/laravel-translatable for all content fields.
Translations are stored as JSON columns in the database.
Translatable Fields by Model
| Model | Translatable Fields |
|---|---|
Post |
slug, title, content, excerpt, meta_title, meta_description |
PostCategory |
slug, name, description |
PostTag |
slug, name |
Slug Resolution
Front-end controllers resolve slugs by searching in the current locale first, then falling back to English:
Featured Images
Posts support a single featured image via a polymorphic MorphOne relationship
to the core Media model (mediable). The HasMedia trait is included on the Post model.
Upload Flow
- Admin uploads an image file via the post create/edit form.
- The
PostServicedelegates toMediaService::uploadFor(). - The image is stored in the
postssubdirectory of the media disk. - Thumbnails are generated based on config dimensions (
featured_images.thumbnail_width/height).
Image Removal
The edit form includes a “Remove featured image” checkbox. When checked,
the existing media record and files are deleted via MediaService::delete().
Uploading a new image automatically replaces the old one.
Email Notifications
The blog add-on sends two types of email notifications:
New Comment Notification
Sent to all admin users (is_admin = true) when a new comment or reply is posted.
- Subject: “New comment on {post title}” or “New reply on {post title}”
- Body: Author name, post title, content preview (200 chars)
- Action: “Moderate Comments” link (if pending) or “View Comment” link (if approved)
Controlled by: blog_notify_admin_on_comment setting.
Comment Reply Notification
Sent to the parent comment’s author when someone replies to their comment.
- Subject: “New reply to your comment on {post title}”
- Body: Reply author name, post title, content preview
- Action: “View Reply” link
Controlled by: blog_notify_author_on_reply setting.
Reply notifications respect the commenter’s notify_on_reply preference
and are not sent when someone replies to their own comment.
CAPTCHA Protection
When blog_captcha_enabled is set to true, the comment form includes
a CAPTCHA challenge. The blog add-on integrates with the core CaptchaService:
- The service checks if CAPTCHA is enabled for the
blogcontext. - The appropriate CAPTCHA field name is resolved via
CaptchaService::getResponseFieldName(). - Validation uses the
CaptchaRuleclass from the core.
A CAPTCHA provider (e.g., reCAPTCHA, hCaptcha) must be configured in the core settings for this feature to work.
Updating
Step 1: Replace Files
Replace the add-on directory with the new version.
Step 2: Run Migrations
Step 3: Clear Caches
Step 4: Rebuild Assets
Only needed if the update includes new or modified SCSS/JS theme files.
Step 5: Verify
Visit Blog → Settings to confirm the settings page loads correctly, then check the front-end blog listing page.
Troubleshooting
Blog pages return 404
- Ensure the Blog add-on is activated in Admin → Add-ons.
- Run
php artisan route:clearto clear the route cache. - Verify the
BlogServiceProvideris being registered (checkAddonServiceProviderautoloader).
Post shows “Not Found” despite being published
- Check that the post status is
published(notdraft). - Ensure
published_atis set and is in the past (future-dated posts are not visible). - Verify the slug matches the URL: slugs are locale-specific. The system tries the current locale first, then English.
Category page returns 404
- Ensure the category is active (
is_active = true). - Check that the category’s
categorizable_typeispost. - Verify the slug in the URL matches the category’s translatable slug for the current locale.
Comments not appearing on posts
- Check that
blog_comments_enabledis set totruein settings. - If using moderation, comments must be approved first. Check the admin comments page for pending items.
- Guest comments require
blog_allow_guest_commentsto be enabled.
Comment form not showing for guests
- Enable
blog_allow_guest_commentsin Blog → Settings. - The
StoreCommentRequestchecks authorization: if guest comments are disabled, the form submission returns 403.
CAPTCHA not appearing on comment form
- Ensure
blog_captcha_enabledis set totruein blog settings. - A CAPTCHA provider must be configured in Admin → Settings → CAPTCHA.
- The
CaptchaService::isEnabledFor('blog')check must returntrue.
Email notifications not being sent
- Check that mail is configured correctly in Admin → Settings → Mail.
- Verify the notification toggles are enabled:
blog_notify_admin_on_commentand/orblog_notify_author_on_reply. - Reply notifications require the parent comment author to have
notify_on_reply = true. - Self-replies do not trigger notifications (by design).
Cannot delete a category: “Cannot delete category with existing posts”
The add-on prevents deleting categories that have posts assigned to them. Either reassign the posts to a different category or delete them first.
Featured image not displaying
- Check that the media file was uploaded successfully (look in the
mediatable for amediable_typematching the post). - Verify the storage symlink exists:
php artisan storage:link. - Check file permissions on the storage directory.
Search returns no results despite matching posts
- The search uses MySQL
JSON_SEARCHwhich requires MySQL 8.0+. - Search is pattern-matched (contains), so partial matches should work.
- Only published posts (with
published_atin the past) are included in search results.
Reading time shows “1 min” for all posts
- Ensure the post has content (the reading time is calculated from
strip_tags(content)). - Check the
blog_words_per_minutesetting: the default is 200 WPM. - Very short posts will always show 1 minute (the minimum).