ArtisanPack UI
DocsCMS FrameworkContent Types Developer Guide

Content Types Developer Guide

CMS Framework v2.16.0

Content types allow you to define custom post types in Digital Shopfront CMS. This guide covers how to register content types programmatically, configure...

Overview

Content types allow you to define custom post types in Digital Shopfront CMS. This guide covers how to register content types programmatically, configure their behavior, and integrate them with the admin interface.

Table of Contents

Registering Content Types

Via Service Provider

Register content types in your service provider's boot() method:

<?php

namespace App\Providers;

use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Managers\ContentTypeManager;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $contentTypeManager = app(ContentTypeManager::class);

        $contentTypeManager->register([
            'name' => 'Products',
            'slug' => 'products',
            'table_name' => 'products',
            'model_class' => 'App\\Models\\Product',
            'description' => 'Manage your product catalog',
            'hierarchical' => false,
            'has_archive' => true,
            'archive_slug' => 'shop',
            'supports' => ['title', 'content', 'excerpt', 'featured_image'],
            'public' => true,
            'show_in_admin' => true,
            'icon' => 'fas-shopping-cart',
            'menu_position' => 30,
            'admin_pages' => [
                'listing' => [
                    'enabled' => true,
                    'title' => 'Products',
                    'slug' => 'products',
                    'component' => 'Modules\\Shop\\Livewire\\Products\\Index',
                    'capability' => 'products.view',
                    'icon' => 'fas-shopping-cart',
                    'position' => 30,
                ],
                'create' => [
                    'enabled' => true,
                    'title' => 'Add New Product',
                    'slug' => 'products.create',
                    'component' => 'Modules\\Shop\\Livewire\\Products\\Create',
                    'capability' => 'products.create',
                    'parent_slug' => 'products',
                ],
                'edit' => [
                    'enabled' => true,
                    'title' => 'Edit Product',
                    'slug' => 'products.edit',
                    'component' => 'Modules\\Shop\\Livewire\\Products\\Edit',
                    'capability' => 'products.edit',
                    'parent_slug' => 'products',
                    'hidden' => true,
                ],
            ],
        ]);
    }
}

Via Filter Hook

Register content types using the filter hook (useful for plugins):

addFilter('ap.contentTypes.registeredContentTypes', function ($contentTypes) {
    $contentTypes['products'] = [
        'name' => 'Products',
        'slug' => 'products',
        'table_name' => 'products',
        'model_class' => 'App\\Models\\Product',
        'supports' => ['title', 'content', 'featured_image'],
        'has_archive' => true,
        'archive_slug' => 'shop',
        'public' => true,
        'show_in_admin' => true,
    ];

    return $contentTypes;
});

Via Database

Create content types through the admin UI or via API:

$contentType = ContentType::create([
    'name' => 'Events',
    'slug' => 'events',
    'table_name' => 'events',
    'model_class' => 'App\\Models\\Event',
    'hierarchical' => false,
    'has_archive' => true,
    'public' => true,
    'show_in_admin' => true,
]);

Available Options

Required Options

Option Type Description
name string Display name, conventionally the plural label (e.g., "Blog Posts")
slug string Machine name (e.g., "posts")
table_name string Database table name (e.g., "posts")
model_class string Fully qualified model class

Optional Options

Option Type Default Description
singular_name string null 2.13.0. Singular label (e.g., "Product"). See Labels
plural_name string null 2.13.0. Plural label (e.g., "Products"). See Labels
labels array null 2.13.0. Label overrides keyed by label name. See Labels
description string null Description shown in admin
hierarchical boolean false Supports parent-child relationships
has_archive boolean true Has archive pages
archive_slug string null Archive URL slug (e.g., "blog")
supports array null Supported features
metadata array null Additional custom settings
public boolean true Visible on frontend
show_in_admin boolean true Show in admin menu
icon string null FontAwesome icon (e.g., "fas-newspaper")
menu_position integer null Admin menu position
admin_pages array null Admin page configurations

Supports Options

The supports array defines which features are available:

'supports' => [
    'title',           // Title field
    'content',         // Content editor
    'excerpt',         // Excerpt field
    'featured_image',  // Featured image
    'author',          // Author assignment
    'comments',        // Comments (if implemented)
    'revisions',       // Revisions (if implemented)
]

Labels

Since 2.13.0. A content type carries a WordPress-style label set so the admin can say "Add New Product" and "Edit Product" instead of reusing name everywhere.

$contentTypeManager->register([
    'name'          => 'Products',
    'singular_name' => 'Product',
    'plural_name'   => 'Products',
    'labels'        => [
        'add_new_item' => 'Add a Product',
    ],
    'slug'          => 'products',
    // ...
]);

$contentType = $contentTypeManager->getContentType('products');

$contentType->getSingularLabel();          // "Product"
$contentType->getPluralLabel();            // "Products"
$contentType->getLabel('edit_item');       // "Edit Product"
$contentType->getLabel('add_new_item');    // "Add a Product" ( override )
$contentType->getLabels();                 // the full set below

Fallbacks

singular_name and plural_name are optional:

  • Singular falls back to Str::singular( name ).
  • Plural falls back to Str::plural( singular_name ) when a singular is set, otherwise Str::plural( name ).

The inflector leaves a word unchanged when it is already in the requested form, so a plural name ( "Pages", "Blog Posts" ) and a singular one ( "Package" ) both resolve correctly. It can guess wrong for some words, e.g. "Media" singularizes to "Medium" and "FAQ" pluralizes to "FAQS". Set the labels explicitly when that matters.

The 2.13.0 migration backfills singular_name and plural_name for existing rows with these same rules. Run php artisan migrate after upgrading.

Label keys

Key Default
singular_name singular label
plural_name plural label
menu_name plural label
add_new "Add New"
add_new_item "Add New {singular}"
new_item "New {singular}"
edit_item "Edit {singular}"
view_item "View {singular}"
view_items "View {plural}"
all_items "All {plural}"
search_items "Search {plural}"
not_found "No {plural} found."
not_found_in_trash "No {plural} found in Trash."
parent_item_colon "Parent {singular}:"
archives "{singular} Archives"

Defaults are translatable ( __() ). A non-empty string in labels replaces the default for its key. Blank, null and non-string values are ignored. Extra keys are passed through for consumers that define their own labels.

API

The content type endpoints accept singular_name, plural_name ( each a nullable string, max 255 ) and labels ( a nullable object of at most 50 entries; each key must match ^[a-z][a-z0-9_]{0,63}$ and each value is a nullable string, max 255 ). Responses include the stored singular_name, plural_name and labels, plus resolved_labels: the full label set with fallbacks and overrides applied.

Escape labels on output. Label values are free-form strings stored by users who can create or update content types. Render them as text, not HTML ( Blade's {{ }}, React's default text rendering ).

Table Naming Conventions

Follow these conventions when naming your content type tables:

1. Use Snake Case

✓ 'table_name' => 'products'
✓ 'table_name' => 'product_reviews'
✗ 'table_name' => 'Products'
✗ 'table_name' => 'productReviews'

2. Use Plural Forms

✓ 'table_name' => 'products'
✓ 'table_name' => 'events'
✗ 'table_name' => 'product'
✗ 'table_name' => 'event'

3. Taxonomy Tables

Prefix taxonomy tables with the content type:

✓ 'product_categories'
✓ 'product_tags'
✓ 'event_types'
✗ 'categories'
✗ 'tags'

4. Pivot Tables

Use descriptive names for pivot tables:

✓ 'post_category_pivots'
✓ 'product_tag_pivots'
✗ 'category_post'
✗ 'tag_product'

Admin Pages Configuration

Admin pages allow automatic registration of CRUD interfaces in the admin menu.

Configuration Structure

'admin_pages' => [
    'listing' => [
        'enabled' => true,
        'title' => 'Products',                              // Menu title
        'slug' => 'products',                               // Route slug
        'component' => 'Modules\\Shop\\Livewire\\Products\\Index',
        'capability' => 'products.view',                    // Required capability
        'icon' => 'fas-shopping-cart',                      // FontAwesome icon
        'position' => 30,                                   // Menu position
        'parent_slug' => null,                              // Top-level menu
    ],
    'create' => [
        'enabled' => true,
        'title' => 'Add New',
        'slug' => 'products.create',
        'component' => 'Modules\\Shop\\Livewire\\Products\\Create',
        'capability' => 'products.create',
        'parent_slug' => 'products',                        // Child of listing
    ],
    'edit' => [
        'enabled' => true,
        'title' => 'Edit Product',
        'slug' => 'products.edit',
        'component' => 'Modules\\Shop\\Livewire\\Products\\Edit',
        'capability' => 'products.edit',
        'parent_slug' => 'products',
        'hidden' => true,                                   // Don't show in menu
    ],
]

Routes

Routes are automatically registered based on admin pages configuration:

  • Listing: /admin/{slug}
  • Create: /admin/{slug}/create
  • Edit: /admin/{slug}/{id}/edit

Best Practices

1. Consistent Naming

Use consistent naming across slug, table, and model:

// Good
'slug' => 'products',
'table_name' => 'products',
'model_class' => 'App\\Models\\Product',

// Bad
'slug' => 'product',
'table_name' => 'product_items',
'model_class' => 'App\\Models\\ProductModel',

2. Use Capabilities Format

Follow the feature.capability format:

✓ 'products.view'
✓ 'products.create'
✓ 'products.edit'
✗ 'view_products'
✗ 'can_edit_product'

3. Set Appropriate Defaults

Configure sensible defaults for your content type:

'hierarchical' => false,        // Only true if needed
'has_archive' => true,          // Most content types need archives
'public' => true,               // Unless it's internal only
'show_in_admin' => true,        // Unless managing via code only

4. Create Migration First

Always create the database table before registering the content type:

php artisan make:migration create_products_table

5. Use Admin Pages

Configure admin pages to automatically create admin UI:

'admin_pages' => [
    'listing' => ['enabled' => true, ...],
    'create' => ['enabled' => true, ...],
    'edit' => ['enabled' => true, ...],
]

Examples

Simple Content Type (Events)

$contentTypeManager->register([
    'name' => 'Events',
    'slug' => 'events',
    'table_name' => 'events',
    'model_class' => 'App\\Models\\Event',
    'description' => 'Manage upcoming events',
    'supports' => ['title', 'content', 'excerpt', 'featured_image'],
    'has_archive' => true,
    'archive_slug' => 'events',
    'public' => true,
    'show_in_admin' => true,
    'icon' => 'fas-calendar',
    'menu_position' => 25,
]);

Hierarchical Content Type (Documentation)

$contentTypeManager->register([
    'name' => 'Documentation',
    'slug' => 'docs',
    'table_name' => 'docs',
    'model_class' => 'App\\Models\\Doc',
    'hierarchical' => true,              // Enable parent-child
    'has_archive' => true,
    'archive_slug' => 'documentation',
    'supports' => ['title', 'content'],
    'public' => true,
    'show_in_admin' => true,
    'icon' => 'fas-book',
]);

Internal Content Type (Form Submissions)

$contentTypeManager->register([
    'name' => 'Form Submissions',
    'slug' => 'submissions',
    'table_name' => 'form_submissions',
    'model_class' => 'App\\Models\\FormSubmission',
    'hierarchical' => false,
    'has_archive' => false,              // No public archives
    'public' => false,                   // Admin only
    'show_in_admin' => true,
    'icon' => 'fas-envelope',
    'supports' => [],                    // Custom fields only
]);

Retrieving Content Types

Get All Registered Content Types

$contentTypeManager = app(ContentTypeManager::class);
$contentTypes = $contentTypeManager->getRegisteredContentTypes();

Get Specific Content Type

$contentType = $contentTypeManager->getContentType('products');

Check if Content Type Exists

if ($contentTypeManager->contentTypeExists('products')) {
    // Content type exists
}

Get Model Instance

$contentType = ContentType::where('slug', 'products')->first();
$modelInstance = $contentType->getModelInstance();

Check Supported Features

if ($contentType->supportsFeature('featured_image')) {
    // Content type supports featured images
}

Content Status Enum (v1.1.0)

Content types that support publishing (posts, pages) use the ContentStatus enum instead of plain strings:

use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Enums\ContentStatus;

// Query with enum
$posts = Post::where('status', ContentStatus::Published)->get();

// Use scopes via HasContentStatus trait
$published = Post::published()->get();
$drafts = Post::draft()->get();

// Check status
if ($post->isPublished()) {
    // ...
}

Available statuses: Draft, Published, Scheduled, Private.

See developer/enums for the complete enum reference and developer/traits for the HasContentStatus and HasContentFilters traits.

See Also