Skip to content

Blog Management (Admin) ​

Flow ID: AD-12 Module(s): blog Complexity: Medium Last Updated: 2026-09-14 — proofread fix: corrected three off-by-one citations for sendBlogCommentStatusUpdateEmail()/blogCommentEmailOutcome() from Adv_mailer.php:619-676 to :619-677, matching SY-24-email-dispatch.md

Business Context ​

The blog module provides a full CMS for article publishing with hierarchical categories, author profiles, color-coded tags, optional comment moderation, product linking, builder block integration, and AI content generation. All blog entities use MUI (multi-language) companion tables. The legacy HMVC layer (ecommercen/blog/) handles the admin UI, and a modern domain/REST layer also exists in src/: src/Domains/Cms/Blog/ (Article, Author, Category, Comment, and Tag entities) and src/Rest/Cms/Controllers/ (BlogArticle, BlogAuthor, BlogCategory, BlogComment, BlogTag controllers) with full REST CRUD.

Authorization ​

All blog admin controllers require one of three roles:

RoleConstantArticlesCategoriesTagsAuthorsComments
AdvisableAUTH_ROLE_ADVISABLEFull + slug editFullFullFullFull
AdminAUTH_ROLE_ADMINFullFullFullFullFull
CMSAUTH_ROLE_CMSFullFullFullFull--
MarketingAUTH_ROLE_MARKETING--------Full

Comments controller (Adv_blog_comments_admin) uses AUTH_ROLE_MARKETING instead of AUTH_ROLE_CMS.

Role-gated field access:

  • Metatags (meta_title, meta_keywords, meta_description): Requires canAccess('metatags', $sessionRoles)
  • URL fields (slug, url, redirect_url): Requires canAccess('urlFields', $sessionRoles) -- slug editing on existing records restricted to ADVISABLE role

Article Management ​

Controller: ecommercen/blog/controllers/Adv_blog_admin.php (509 lines) Model: ecommercen/blog/models/Adv_blog_model.phpTables: blog (master), blog_mui (MUI), blog_blog_categories (LP), blog_blog_tags (LP)

Key Methods ​

MethodAction
index($offset)Paginated listing with session-persisted search filters (published, promo, author, keyword)
resetIndex()Clear search filters and redirect
add()Create article with MUI content, category/tag assignments, image upload, blog-product relations
edit($id)Update article; loads record as master+details, re-renders category/tag/product combos
delete($id)Delete article, its MUI records, category/tag link-pairs, and all associated comments
saveBlogProducts($blogId)Save blog-product associations with optional two-way sync

Article Data Fields ​

Master table (blog):

  • slider_id (nullable FK to sliders)
  • blog_author_id (nullable FK to blog_author)
  • youtube_video_id (extracted via extractYouTubeId())
  • spotify_podcast_id
  • is_published (0/1)
  • is_promo (0/1)
  • blog_date (defaults to current date)
  • reading_time
  • banner_image (uploaded to files/blogs/)
  • hits (view counter, incremented on frontend)

MUI table (blog_mui) per language:

  • title (required)
  • description (rich text, unescaped via stripslashes)
  • small_description (rich text excerpt)
  • slug (auto-generated from title on create; editable only by ADVISABLE on update)
  • builder_block_id (nullable FK for builder block integration)
  • meta_title, meta_keywords, meta_description (metatags-gated)
  • url, redirect_url (urlFields-gated)

Slug Generation Logic ​

  • On create: Always auto-generated from title via createSlug()
  • On update: If ADVISABLE role, slug is taken from the POST field (can be manually edited). If slug is empty and no existing slug exists, auto-generated from title. Non-ADVISABLE roles cannot modify the slug.

Search / Filtering ​

Session-stored filters (artSearch session key):

  • published: 'true' / 'false' / null (all)
  • promo: 'true' / 'false' / empty
  • searchAll: Matches against blog.id, blog_mui.slug, or blog_mui.title LIKE
  • author: Author ID filter (where-in on blog_author.id)
  • artLimit: Persisted per-page limit

Relations Management ​

  • Categories: Multi-select via categoryIds[] POST. On create: insertCategoriesLp(). On update: delete-and-reinsert via updateCategoriesLp(). Category is required (validated).
  • Tags: Multi-select via tagIds[] POST. Same delete-and-reinsert pattern via updateTagsLp().
  • Products: Managed via saveBlogProducts() on article save (see Blog-Product Linking below).

Delete Cascade ​

delete($id) removes:

  1. blog master record
  2. blog_mui MUI records
  3. blog_blog_categories link-pair records
  4. blog_blog_tags link-pair records
  5. blog_comments records (via blog_comments_model->deleteBlogComments())

Extensibility Hooks ​

The controller provides empty protected methods for client overrides:

  • beforeAddRecord($data, $dataMui) / beforeEditRecord($id, $data, $dataMui) -- pre-save data mutation
  • afterAdd($id) / afterEdit($id) / afterDelete($id) -- post-save side effects
  • blogRender() / blogAddRender() / blogEditRender() -- inject extra render data

AI Content Generation ​

The article edit view integrates aiContentGenerationTrait (from ecommercen/ai/traits/), which:

  1. Loads prompt config from ai_content_generation_prompts config file
  2. Merges registry overrides from AI_CONTENT_GENERATION_PROMPTS group
  3. Passes JSON state to the admin view with provider, enabled flag, prompts, storeUrl, and storeName

Enabled only when registry key IS_ENABLED in group AI_CONTENT_GENERATION_PROVIDER_OPEN_AI is truthy.


Category Management ​

Controller: ecommercen/blog/controllers/Adv_blog_category_admin.php (390 lines) Model: ecommercen/blog/models/Adv_blog_category_model.phpTables: blog_categories (master), blog_categories_mui (MUI), blog_blog_categories (LP)

Key Methods ​

MethodAction
index($parentId)Hierarchical listing with breadcrumb navigation; counts articles per category
add()Create with parent assignment, 3 images, MUI content
edit($blogCategoryId)Update all fields; supports individual image deletion
delete($blogCategoryId)Delete only if no articles linked (checked via allowedCategoryDeletion)
updateorder()AJAX drag-and-drop reorder via listItem POST array

Category Data Fields ​

Master table (blog_categories):

  • parent_id (0 = root; supports nested hierarchy)
  • is_active (visibility toggle)
  • banner_image, small_banner, promo_banner (3 images, all uploaded to files/blogs/)
  • promo_banner_url (link target for promo banner)
  • ord (ordering position)

MUI table (blog_categories_mui) per language:

  • title (required)
  • description
  • slug (auto-generated on create; editable by ADVISABLE on update)
  • meta_title, meta_keywords, meta_description (metatags-gated)
  • url, redirect_url (urlFields-gated)

Hierarchy ​

Categories support parent-child nesting with recursive breadcrumb building (buildCategoryAncestry()). The listing view shows subcategory count per category and navigates into children via index($parentId).

Business Rules ​

  • Deletion blocked if any articles are linked to the category (allowedCategoryDeletion checks blog_blog_categories)
  • Ordering: Drag-and-drop reorder updates ord column via AJAX
  • Categories are presented as a flat dropdown combo in article forms (via categoriesCombo())

Author Management ​

Controller: ecommercen/blog/controllers/Adv_blog_author_admin.php (359 lines) Model: ecommercen/blog/models/Adv_blog_author_model.phpTables: blog_author (master), blog_author_mui (MUI)

Key Methods ​

MethodAction
index()List all authors with article count per author
add()Create with banner image, MUI name/description/small_description
edit($id)Update; supports image deletion
delete($id)Delete only if no articles assigned (checked via allowedAuthorDeletion)

Author Data Fields ​

Master table (blog_author):

  • banner_image (uploaded to files/blogs/)

MUI table (blog_author_mui) per language:

  • name (required)
  • description
  • small_description
  • slug (auto-generated from name on create; editable by ADVISABLE on update)
  • meta_title, meta_keywords, meta_description (metatags-gated)
  • url, redirect_url (urlFields-gated)

Business Rules ​

  • Deletion blocked if any articles reference the author (allowedAuthorDeletion checks blog table for blog_author_id)
  • Author is optional on articles (nullable blog_author_id)

Tag Management ​

Controller: ecommercen/blog/controllers/Adv_blog_tags_admin.php (357 lines) Model: ecommercen/blog/models/Adv_blog_tags_model.phpTables: blog_tags (master), blog_tags_mui (MUI), blog_blog_tags (LP)

Key Methods ​

MethodAction
index()Sortable list of all tags
add()Create with active flag, hex color, MUI title/description
edit($id)Update; same fields
delete($id)Delete only if no articles linked (checked via allowedTagDeletion)
updateorder()AJAX drag-and-drop reorder

Tag Data Fields ​

Master table (blog_tags):

  • is_active (visibility toggle)
  • ord (ordering position)

MUI table (blog_tags_mui) per language:

  • title (required)
  • description
  • hexcode (hex color code, shared across all languages -- stored in MUI table but same value posted for all)
  • slug (auto-generated from title on create; editable by ADVISABLE on update)
  • meta_title, meta_keywords, meta_description (metatags-gated)
  • url, redirect_url (urlFields-gated)

Business Rules ​

  • Deletion blocked if any articles are tagged with it (allowedTagDeletion checks blog_blog_tags)
  • On successful deletion: success flash message. On blocked deletion: error flash message (eshop_error session key).
  • Tags displayed as multi-select dropdown combo in article forms

Comment Moderation ​

Controller: ecommercen/blog/controllers/Adv_blog_comments_admin.php (166 lines) Model: ecommercen/blog/models/Adv_blog_comments_model.phpTable: blog_comments

Feature Gate ​

The entire comments subsystem is gated by registry setting OTHER.ENABLE_BLOG_COMMENTS. If disabled, the comments admin controller redirects to the admin home page. The article listing also conditionally shows a pending comments badge count.

Key Methods ​

MethodAction
index($offset)Paginated listing of comments with search/filter
resetIndex()Clear search filters
setStatus($commentId, $status)Set comment to approved/rejected/pending + send email notification (no email is sent when the resolved status is pending, per application/models/Adv_mailer.php:619-677)
batchAction()Bulk status update for selected comments

Comment Status Flow ​

pending  -->  approved  (sends blogCommentAccept email)
pending  -->  rejected  (sends blogCommentReject email)
approved -->  rejected  (sends blogCommentReject email)
rejected -->  approved  (sends blogCommentAccept email)
approved -->  pending   (no email sent)
rejected -->  pending   (no email sent)

The pending transitions send nothing: Adv_mailer::blogCommentEmailOutcome() returns null for any status that isn't 1/'1'/'approved' or 2/'2'/'rejected', and sendBlogCommentStatusUpdateEmail() short-circuits on a null outcome (application/models/Adv_mailer.php:619-677).

Email Notification ​

On setStatus(), the controller:

  1. Fetches the comment record (getCurrentRecord)
  2. Builds a customerData object with mail, name, comment, blog_id, lang
  3. Calls Adv_mailer::sendBlogCommentStatusUpdateEmail() which sends via the EMAIL_CONTACT_FORM config using EMAIL_SUBJECTS.BLOG_COMMENT_ACCEPT or EMAIL_SUBJECTS.BLOG_COMMENT_REJECT registry subjects. If the resolved status is pending, blogCommentEmailOutcome() returns null and the method returns early — no email is sent (application/models/Adv_mailer.php:619-677)

Search Filters ​

  • commentStatus: 'pending' (default) / 'true' (approved) / 'false' (rejected) / all
  • searchAll: Matches against comment ID, blog title, commenter name, or comment content
  • searchMail: Email address filter
  • date_start: Date range filter

Comment Data Fields (blog_comments table) ​

  • blog_id (FK to article)
  • name, email (commenter info)
  • comment_content
  • status (pending / approved / rejected)
  • entry_date
  • lang

Blog-Product Linking ​

Controller: ecommercen/blog/controllers/AdvApiBlogProduct.php (140 lines) Model: ecommercen/blog/models/Adv_blog_product_model.phpTable: blog_product (blog_id, product_id, priority)

Feature Gate ​

Gated by registry setting ESHOP.BLOG_PRODUCT_ENABLED. When disabled, the API returns 404 and the article save skips product processing.

API Endpoints (JSON, ApiEndpointTrait) ​

MethodHTTPAction
addProduct()POSTAdd single product to article with priority
deleteProduct()DELETERemove single product from article
repositionProducts()POSTBatch reorder products for an article

All endpoints validate via form_validation with blogId (required int), productId (required int for add/delete), priority (optional int).

Two-Way Sync ​

When ESHOP.BLOG_PRODUCT_TWO_WAY_SYNC is enabled, product-article links are maintained in both directions:

  • blog_product table: blog-side (managed by Adv_blog_product_model)
  • product_blog table: product-side (managed by Adv_product_blog_model in ecommercen/eshop/models/)

Both the API controller and the article save method (saveBlogProducts) honor this sync. The product admin also uses product_blog_model for the reverse direction.

Product Display in Article Edit ​

renderBlogProducts() enriches linked products with live data (stock, codes, images) via helper functions (getLiveProductsParsed, getProductsCodesParsed, getProductsCodeImagesParsed) and sorts by priority.


Database Schema ​

Tables ​

TableTypeDescription
blogMasterArticle records
blog_muiMUIArticle multi-language content
blog_categoriesMasterCategory hierarchy
blog_categories_muiMUICategory multi-language content
blog_authorMasterAuthor profiles
blog_author_muiMUIAuthor multi-language content
blog_tagsMasterTags with ordering
blog_tags_muiMUITag multi-language content with hex color
blog_blog_categoriesLink-pairArticle-to-category many-to-many
blog_blog_tagsLink-pairArticle-to-tag many-to-many
blog_productLink-pairArticle-to-product with priority
product_blogLink-pairProduct-to-article (reverse sync)
blog_commentsFlatUser-submitted comments with moderation status
blog_defaultMasterDefault blog page settings
blog_default_muiMUIDefault blog page MUI content

Registry Settings ​

GroupKeyEffect
ESHOPBLOG_PRODUCT_ENABLEDEnables blog-product linking feature
ESHOPBLOG_PRODUCT_TWO_WAY_SYNCEnables bidirectional sync between blog_product and product_blog
ESHOPBLOG_PRODUCT_CATEGORY_ENABLEDEnables blog-product-category cross-linking
OTHERENABLE_BLOG_COMMENTSEnables comment submission and moderation
EMAIL_SUBJECTSBLOG_COMMENT_ACCEPTEmail subject for comment approval notification
EMAIL_SUBJECTSBLOG_COMMENT_REJECTEmail subject for comment rejection notification
AI_CONTENT_GENERATION_PROVIDER_OPEN_AIIS_ENABLEDEnables AI content generation in article editor

File Upload ​

All blog images are uploaded to files/blogs/ via the advuploader library. Upload methods validate file types and report errors to the view. Image deletion clears the DB field and calls storage()->delete() on the physical file.


Routes ​

php
// application/config/my_routes.php
$route['smileblog']        = 'blog';
$route['smileblog/(:any)'] = 'blog/$1';

Admin routes follow standard HMVC convention: blog/blog_admin, blog/blog_category_admin, blog/blog_author_admin, blog/blog_tags_admin, blog/blog_comments_admin, blog/advapiblogproduct.