Skip to main content

Documentation

No results found.
Features

Blog

WebProCMS ships with a built-in multi-author blog: posts with featured image, gallery, categories, slug, published date, CTA buttons, and SEO metadata. Posts are first-class records (not custom content type rows), so the public site has a d...

⚠️ Partially outdated (2026-06-29). The standalone Post subsystem described below was folded into the content-type system (type_slug = blog on content_items, managed at Dashboard → Content). The reader-facing behaviour (index, detail pages, categories, comments, content blocks, SEO) still works as described, but implementation specifics — the posts table, dashboard.blog.* routes, posts:* commands — no longer exist. Rewrite pending.

WebProCMS ships with a built-in multi-author blog: posts with featured image, gallery, categories, slug, published date, CTA buttons, and SEO metadata. Posts are first-class records (not custom content type rows), so the public site has a dedicated /blog index, dedicated post detail pages, dedicated design library row templates, and category-aware filtering on both sides. Managed at Dashboard → Blog.


The problem

A "blog" inside a CMS sits in an awkward spot. If it's modelled as a custom content type it loses the shape conventions that blogs have agreed on for two decades — featured image, excerpt, categories, author byline, RSS-ready ordering — and every site has to re-invent them. If it's modelled as a hardcoded posts table that ignores the rest of the system, it can't share the media library, can't share the design library, and can't be styled by editors without a developer.

The trick is to make the blog feel native — opinionated about the shape, pre-wired to media and design — while still composing cleanly with everything else. Posts are not a content type, but they share every infrastructure piece a content type uses.

The fix

Post is a regular Eloquent model with a fixed schema covering the conventions everyone expects: title, slug, excerpt, content, status, published_at, featured_media_id, layout, plus the standard SEO fields (meta_title, meta_description, is_noindex, og_*). Multi-instance CTA / gallery / FAQ content blocks live in a separate polymorphic table (see "Content blocks" below). Slugs are auto-generated from the title with collision suffixing. Status supports draft, published, unlisted, unpublished. Saves and deletes clear the response cache automatically.

Categories are a separate Category model with a posts() HasMany. Each post belongs to one category (category_id FK). Categories are renameable; slugs auto-update on save. Public-site listing rows can filter by category.

featured_media_id is a foreign key to media_items, not a string path. There's no separate uploader on the post edit page — the "Pick from media library" button on ⚡edit.blade.php opens the standard media picker modal, the picker dispatches media-image-picked, and the post stores the FK. Replacing or deleting the media item flows through MediaItem::usageSummary() so the user is warned when an image is in use, with cache busting handled by MediaItem::url() appending ?v={updated_at}.

Post::featuredImageUrl() is the canonical accessor; featuredImagePath() returns the storage path for the rare case the raw path is needed (e.g. for AI generation references).

A Generate with AI sparkles button sits next to the Featured Image label on the post edit page. Clicking it opens a modal with the same shape as the page-editor AI flow:

  • Prompt — free-form description of the image to generate.
  • Use this post's title, excerpt and content as context — checkbox, enabled by default. When checked, the title, excerpt, and first ~600 chars of stripped content are appended to the prompt so the generated image reflects what this specific post is about. A "see what context would be sent" toggle previews the exact string before generation.
  • Attach reference image — image-to-image iteration via OpenAI's /v1/images/edits endpoint. Hidden for non-OpenAI providers.
  • Advanced overrides — provider-aware: size + quality for OpenAI, aspect ratio for Stability / Google, image_size + inference steps for fal.ai.
  • Tweak / Generate fresh / Use as featured image — after generation, the preview shows below the prompt with the same buttons the page editor exposes. Saving moves the temp file into the blog media category, derives alt text via the configured text provider's vision API, creates a MediaItem, and sets the post's featured_media_id automatically.

The provider request path is identical to the page editor's — same memory-safe streaming, same per-provider request shapes, same WebP normalization for OpenAI. Both flows share AiImageGenerator. See AI Image Generation for the full feature contract.

Content blocks: multiple CTAs, galleries, and FAQs

A post can have any number of three block types — CTA button groups, photo galleries, and FAQ sets — managed together in the Content Blocks panel on the post edit screen (a reusable <livewire:content-blocks-manager> component). Each block has a friendly label, an editable shortcode slug, and a per-block auto-append toggle:

  • Auto-append on (default) → the block renders at the end of the post body, in the shared block order (drag blocks with the click-to-move control). Multiple auto-append blocks of different types interleave by that single order.
  • Auto-append off (manual) → the block renders only where you place its shortcode [[type:slug]] in the body — e.g. [[gallery:summer-trip]], [[cta:signup]], [[faq:pricing]]. A placed shortcode also suppresses auto-append for that block, so you never get it twice.

Blocks are stored polymorphically in content_blocks (blockable morph → Post now; Events/Content Items reuse the same model via the HasContentBlocks trait). CTA buttons live in the block's settings.buttons; galleries reference the media library through the content_block_media pivot (columns 2–5 in settings); a FAQ block IS the faqable, so each block owns its own faqs rows and reuses the existing FAQ editor. Block text (CTA button labels, FAQ questions/answers) is translatable per site language. The public side renders galleries as a responsive click-to-zoom grid and ships FAQPage JSON-LD for FAQ blocks. All three flow through ShortcodeProcessor::processBodyContent(). See Inline blocks: FAQs and photo galleries for the rendering contract.

New posts use auto-draft. "New Post" (the dashboard.blog.new route) creates a blank draft up front and drops you straight into the editor, so content blocks — which attach to a saved post — are available immediately. "View Post" works on a draft: managers/admins can preview a draft (or scheduled / unpublished) post at its public URL, while guests still get a 404. Logged-in requests bypass the response cache (GuestOnlyCacheProfile), so a draft preview is never cached or leaked; "Preview as guest" falls back to the public 404. The placeholder auto-draft-* slug is regenerated from the title on first save (unless you've customised it), and abandoned empty drafts (no title, no body, no edits) are swept daily by posts:gc-drafts. The body is optional — a post can be gallery-only or FAQ-only, with its content blocks carrying everything; an empty body is stored as ''.

Public routes and detail pages

The public side has two top-level pages:

  • resources/views/pages/blog/⚡index.blade.php — the index page. Composed of design library rows the editor can reorder and restyle.
  • resources/views/pages/blog/⚡show.blade.php — the per-post detail page, resolved by slug.

Both routes are registered in routes/web.php and benefit from the same CacheResponse middleware as the rest of the public site.

Dedicated design library rows

The design library ships with a dedicated blog/ row category at resources/design-library/rows/blog/ — twelve templates including blog-grid, blog-list, blog-magazine, blog-featured, blog-carousel, blog-with-sidebar, blog-large-cards, blog-compact, blog-dark, blog-minimal, blog-two-column, and a blog-posts-index listing template. Each binds to the content_type:blog data source (ContentTypePreset) so editors can drop a blog row onto any page (homepage, sidebar, footer) and it pulls real posts. Categories surface through TaxonomyTermsPreset (taxonomy_blog_category) for category-grid widgets.

Comments

Blog posts support social-login comments out of the box — threaded one level deep, with moderation, ban/whitelist, and an optional email digest. Comments are a free core feature (not the paid Memberships add-on): commenters sign in with Google, GitHub, Facebook, X, Microsoft, Apple, or Discord through a dedicated commenters auth guard, and their avatars are pulled from the social profile. There are no anonymous comments and no passwords to manage.

Performance-wise the approved comment list is server-rendered straight into the response-cached post page (crawlable, zero query on a cache hit), while only the per-visitor composer is cookie-gated so guests fire no extra requests. Managed at Dashboard → Comments, with moderation policy and provider credentials at Dashboard → Settings → Comments. See Blog Comments for the full feature contract.

Status semantics

Status In published scope In accessible scope Public visibility
published yes yes listed and reachable
unlisted no yes reachable by direct URL only
draft no no dashboard-only
unpublished no no dashboard-only

The public index uses Post::scopePublished(). Per-post show pages use scopeAccessible() so unlisted URLs work for sharing without surfacing in lists.

What lives where

Path Purpose
app/Models/Post.php The post model — featured media FK, gallery pivot, slug auto-generation, status scopes, response-cache invalidation on save/delete.
app/Models/Category.php The category model — auto-slugged on save, posts() HasMany.
app/Models/Comment.php Blog comments — threaded, moderated, social-login authored. See Blog Comments.
resources/views/pages/dashboard/blog/ Dashboard CRUD — ⚡index, ⚡create, ⚡edit.
resources/views/pages/blog/⚡index.blade.php Public blog index page (composed of design library rows).
resources/views/pages/blog/⚡show.blade.php Public per-post detail page.
resources/design-library/rows/blog/ Twelve dedicated blog row templates.
app/Support/DataSources/Presets/ContentTypePreset.php Posts data source for design library rows (content_type:blog — blog is a content type).
app/Support/DataSources/Presets/TaxonomyTermsPreset.php Categories/tags data source (taxonomy_blog_category, taxonomy_blog_tag).

Schema reference

Column Notes
title Required.
slug Auto-generated from title with -1, -2… suffixing on collision.
excerpt Short summary for cards and SEO descriptions.
content Long body (HTML). Supports shortcodes via ShortcodeProcessor.
category_id FK to categories; nullable.
featured_media_id FK to media_items; nulled on delete of source media.
content_blocks (table) Polymorphic CTA / gallery / FAQ blocks — see "Content blocks" above. CTA buttons + gallery columns live in settings; galleries use the content_block_media pivot; FAQ blocks own faqs rows.
layout image-top or image-right — controls the show page top section layout.
status draft / published / unlisted / unpublished.
published_at Timestamp; ordering hook for the public index.
meta_title, meta_description, is_noindex, og_* Standard SEO metadata.
is_seeded Marks demo posts so they can be cleanly removed when the install is configured for real use.