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_SEARCH in translatable slug lookups)
  • The core categories table must exist (blog categories use the unified categories table with categorizable_type = 'post')
Note: The blog add-on is standalone and has no dependencies on other add-ons. It integrates with the core media library for featured image management.

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 draft or published
  • 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
Auto-slug generation: If the slug field is left empty for any locale, it is automatically generated from the title using 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.
Deletion guard: A category cannot be deleted if it has existing posts. Reassign or delete the posts first.

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).

Note: When a tag is deleted, all post-tag associations are removed (via 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:

  1. Posts in the same category
  2. 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.

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

  1. If Require Approval is off → all comments are auto-approved.
  2. If the commenter is authenticated → auto-approved.
  3. If Auto-Approve Trusted is on and the email has a previously approved comment → auto-approved.
  4. 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

  1. Admin uploads an image file via the post create/edit form.
  2. The PostService delegates to MediaService::uploadFor().
  3. The image is stored in the posts subdirectory of the media disk.
  4. 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 blog context.
  • The appropriate CAPTCHA field name is resolved via CaptchaService::getResponseFieldName().
  • Validation uses the CaptchaRule class 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.

Backup first: Always back up your database before running migrations on a production system.

Troubleshooting

Blog pages return 404

  • Ensure the Blog add-on is activated in Admin → Add-ons.
  • Run php artisan route:clear to clear the route cache.
  • Verify the BlogServiceProvider is being registered (check AddonServiceProvider autoloader).

Post shows “Not Found” despite being published

  • Check that the post status is published (not draft).
  • Ensure published_at is 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_type is post.
  • 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_enabled is set to true in settings.
  • If using moderation, comments must be approved first. Check the admin comments page for pending items.
  • Guest comments require blog_allow_guest_comments to be enabled.

Comment form not showing for guests

  • Enable blog_allow_guest_comments in Blog → Settings.
  • The StoreCommentRequest checks authorization: if guest comments are disabled, the form submission returns 403.

CAPTCHA not appearing on comment form

  • Ensure blog_captcha_enabled is set to true in blog settings.
  • A CAPTCHA provider must be configured in Admin → Settings → CAPTCHA.
  • The CaptchaService::isEnabledFor('blog') check must return true.

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_comment and/or blog_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 media table for a mediable_type matching 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_SEARCH which requires MySQL 8.0+.
  • Search is pattern-matched (contains), so partial matches should work.
  • Only published posts (with published_at in 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_minute setting: the default is 200 WPM.
  • Very short posts will always show 1 minute (the minimum).

Was this article helpful?

Thank you for your feedback!

Still need help? Create a support ticket

Create a Ticket