Create a PagibleAI Theme Package

Create a separate Composer package when you want to reuse a PagibleAI design across applications or maintain it independently from one Laravel project. Your package owns the theme metadata, main document layout, CSS overrides, optional content-element views and translations. PagibleAI supplies the base page-type layouts, elements and assets around it.

This guide builds a theme named Atlas with the identifier atlas. Replace the example vendor, package, namespace and theme name with your own values, but keep the identifier consistent everywhere.

Compatibility: This guide uses aimeos/pagible-theme:~0.12, covering compatible PagibleAI 0.12+ versions without crossing the next major version.

Name and scaffold the package

Use a short lowercase identifier containing letters, numbers or hyphens. The example uses atlas.

  • Theme identifier: atlas — used by PagibleAI, the Blade namespace and public/vendor/cms/atlas/.
  • Composer package: acme/pagible-theme-atlas — the name applications install.
  • PHP namespace: Acme\Pagible\Atlas — prevents class collisions with PagibleAI and other themes.
  • Editor label: Atlas — the human-readable name shown to editors.

A mismatch between the registered identifier and the asset or view namespace is the most common reason a new theme does not render.

pagible-theme-atlas/
├── composer.json
├── README.md
├── LICENSE
├── schema.json
├── preview.webp                         # optional editor preview
├── src/
│   └── AtlasServiceProvider.php
├── public/
│   ├── cms.css
│   └── cms-lazy.css
├── views/
│   └── layouts/
│       └── main.blade.php
├── lang/
│   └── de.json                          # optional translations
├── tests/                               # optional automated tests
│   └── AtlasThemeTest.php
└── database/
    └── seeders/
        └── AtlasDemo.php               # optional demo

schema.json, the service provider and views/layouts/main.blade.php form the required package boundary. A selected base page-type layout eventually extends <theme>::layouts.main, so copy the current base document layout before changing its markup.

Only ship CSS and element views that your design changes. Theme assets and standard content elements fall back to aimeos/pagible-theme when your package does not override them.

Register the Composer package

{
    "name": "acme/pagible-theme-atlas",
    "description": "Atlas theme for PagibleAI CMS",
    "keywords": ["laravel", "cms", "pagible", "theme"],
    "type": "library",
    "license": "MIT",
    "require": {
        "php": "^8.2",
        "aimeos/pagible-theme": "~0.12"
    },
    "autoload": {
        "psr-4": {
            "Acme\\Pagible\\Atlas\\": "src/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Acme\\Pagible\\Atlas\\AtlasServiceProvider"
            ]
        }
    }
}

Laravel discovers the service provider from extra.laravel.providers; you do not register it manually. The manifest above contains only the package's required runtime wiring. Add development dependencies and seeder autoloading only if you use the optional sections below.

<?php

namespace Acme\Pagible\Atlas;

use Aimeos\Cms\Schema;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

class AtlasServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $basedir = dirname(__DIR__);

        Schema::register($basedir, 'atlas');
        View::addNamespace('atlas', $basedir . '/views');
        $this->loadJsonTranslationsFrom($basedir . '/lang');

        $this->publishes(
            [$basedir . '/public' => public_path('vendor/cms/atlas')],
            'cms-theme'
        );
    }
}

Schema::register() makes Atlas available to editors. The Blade namespace and published asset directory use the same identifier. loadJsonTranslationsFrom() loads JSON translations used by __() calls in Blade; remove it when the package has no lang/ directory.

The provider is safe for Laravel Octane because it only registers package definitions during boot. Do not store request or tenant data in static properties.

Define schema.json

The schema describes the theme in the editor and declares the editable sections for each supported page type. A section such as footer must match the data-section="footer" region rendered by its Blade layout.

{
    "label": "Atlas",
    "description": "Editorial theme for product and documentation sites",
    "maintainer": "Acme",
    "email": "web@example.com",
    "website": "https://example.com",
    "types": {
        "page": {
            "sections": ["main", "footer"]
        },
        "docs": {
            "sections": ["main", "footer"]
        },
        "blog": {
            "sections": ["main", "footer"]
        }
    },
    "config": {
        "theme": {
            "group": "theme",
            "fields": {
                "--pico-primary": {
                    "type": "color",
                    "label": "Primary color",
                    "default": "#3771C8"
                },
                "--pico-border-radius": {
                    "type": "string",
                    "label": "Border radius",
                    "default": "0.25rem"
                }
            }
        }
    }
}

PagibleAI prefixes content, metadata and configuration entries from non-base themes. A custom quote content type therefore becomes atlas::quote, preventing collisions with another package's quote type.

Place an optional preview.webp beside schema.json for the theme selector. Keep the schema below 1 MiB, use a valid https:// website URL and translate every label editors or visitors will see.

Build layouts, assets and elements

Copy the installed base document layout to views/layouts/main.blade.php, then change only the structure and classes your design needs. Set a body class such as theme-atlas so CSS stays scoped.

Keep metadata, canonical links, JSON-LD, the Content Security Policy, translated navigation and accessibility labels, cmstheme() calls, @stack('head'), @yield('main'), @yield('footer') and @include('cms::layouts.foot'). Use @@context and @@type inside Blade JSON-LD. Removing these contracts can disable editing, navigation filtering, cache-safe CSP output or lazy-loaded assets.

:root {
    --pico-primary: #3771C8;
    --pico-primary-hover: #2DA3CD;
    --pico-border-radius: 0.25rem;
}

.theme-atlas .hero h1 {
    max-width: 16ch;
}

.theme-atlas footer {
    border-top: 1px solid var(--pico-muted-border-color);
}

The base layout loads Pico CSS before cms.css. cmstheme($page, 'cms.css') checks public/vendor/cms/atlas/cms.css first and falls back to the base file, so your package can override assets selectively.

For structural changes to an existing element, add the matching Blade file such as views/hero.blade.php; PagibleAI tries atlas::hero before cms::hero. New schema entries need matching views. Keep output escaped, use @text or @markdown for the intended Markdown boundary, load files with cmsfile() and build page-aware URLs with cmsasset().

See Create PagibleAI Content Elements for field schemas, file handling and validation.

Install and activate locally

Use a Composer path repository while the Laravel application and theme live on your machine. Run these commands in the application, adjusting the relative path:

composer config repositories.atlas path ../pagible-theme-atlas
composer require acme/pagible-theme-atlas:@dev
php artisan vendor:publish --tag=cms-theme
php artisan optimize:clear

Package discovery boots the provider and publication copies assets to public/vendor/cms/atlas/. Re-run the publish command after adding an asset; add --force only when you intend to overwrite an existing published file.

Open a page in the PagibleAI admin and select Atlas. Apply it to each page that should use the package, or set its theme value in a seeder or importer. Check every page type declared in schema.json; type-specific layouts fall back to the base package but still extend atlas::layouts.main.

Optional demo content

This fragment is not a complete manifest. Merge the Database\Seeders\ entry into the existing PSR-4 map in composer.json; do not replace the complete file:

{
    "autoload": {
        "psr-4": {
            "Acme\\Pagible\\Atlas\\": "src/",
            "Database\\Seeders\\": "database/seeders/"
        }
    }
}

Create Database\Seeders\AtlasDemo, then run composer dump-autoload. The class name must be the StudlyCase theme identifier followed by Demo so cms:demo can find it.

Run php artisan cms:demo --theme=atlas --tenant=atlas only in development because it writes pages, files and versions to that tenant.

Validate the package

Run these quick checks before you install or share the package. They catch invalid Composer metadata, malformed schema JSON and PHP syntax errors without requiring an automated test suite:

composer validate --strict
php -r 'json_decode(file_get_contents("schema.json"), true, flags: JSON_THROW_ON_ERROR); echo "schema.json OK\n";'
php -l src/AtlasServiceProvider.php

Optional layout regression test

Automated tests are optional. This fragment is not a complete manifest. Merge its Testbench dependency and test namespace into your existing composer.json; do not replace the complete file:

{
    "require-dev": {
        "orchestra/testbench": "^9.0||^10.0||^11.0"
    },
    "autoload-dev": {
        "psr-4": {
            "Acme\\Pagible\\Atlas\\Tests\\": "tests/"
        }
    }
}

Run composer update, then save this focused contract test as tests/AtlasThemeTest.php:

<?php

namespace Acme\Pagible\Atlas\Tests;

use Acme\Pagible\Atlas\AtlasServiceProvider;
use Aimeos\Cms\Schema;
use Aimeos\Cms\ThemeServiceProvider;
use Orchestra\Testbench\TestCase;

class AtlasThemeTest extends TestCase
{
    public function testThemeRegistersMainLayout(): void
    {
        $theme = Schema::get('atlas') ?? [];

        $this->assertSame('Atlas', $theme['label'] ?? null);
        $this->assertTrue(view()->exists('atlas::layouts.main'));
    }

    protected function getPackageProviders($app): array
    {
        return [ThemeServiceProvider::class, AtlasServiceProvider::class];
    }
}

Run the test with vendor/bin/phpunit tests. It deliberately checks only the stable registration and layout contracts. Add route-level tests when your package-specific page types or elements render differently from the base theme. You can also run vendor/bin/phpstan analyze src tests when PHPStan is installed.

Before tagging the package, install it in a clean PagibleAI 0.12+ application. Confirm that the theme appears once, every declared page type renders, published assets exist, omitted assets and standard elements fall back, custom elements use their own views, translations and RTL layouts work, frontend editing remains available, and the browser reports no CSP or missing-asset errors.

If you maintain automated checks, run the host application's PHPUnit and PHPStan suites across the Laravel versions the package supports. Publish theme assets as part of deployment.

References

Verified snapshot: This guide was checked against commit 023b76d on 2026-08-02. Its examples follow the Pagible theme package manifest, theme service provider, theme schema, schema registry and view and asset helpers.

Troubleshooting FAQ

Why is Atlas missing from the editor?

Run composer dump-autoload, confirm package discovery lists AtlasServiceProvider and validate schema.json.

Why is `atlas::layouts.main` missing?

Add views/layouts/main.blade.php and confirm the service provider registers the atlas view namespace.

Why does a theme asset return 404?

Publish the cms-theme tag and check the exact case-sensitive filename below public/vendor/cms/atlas/.

Why does a custom element use the base view?

Confirm that the schema key, stored atlas::... type and Blade filename match.

Why don't my changes appear?

Run php artisan optimize:clear, republish changed assets and hard-refresh your browser.

Why is an external asset blocked?

Add only its trusted host to the matching CMS_CSP_* setting. Keep the Content Security Policy enabled.

Where are the theme template contracts documented?

See Customize PagibleAI Theme for navigation, breadcrumbs, caching, CSP and template helper contracts.