Skip to content

Creating a Custom Theme Package

This page moves the custom-theme folder from Creating a Store Theme into the Webkul\CustomTheme package. A package can be versioned and installed on other stores, and it's where the theme's Vite build, section types and image templates live on later pages.

What You'll Build

text
packages/Webkul/CustomTheme/
└── src/
    ├── Providers/
    │   └── CustomThemeServiceProvider.php
    └── Resources/
        ├── lang/
        │   └── en/
        │       └── app.php
        └── views/
            └── home/
                └── index.blade.php

The package keeps using the custom-theme entry in config/themes.php from Step 1 of the previous page. Add that entry now if you skipped it.

Step 1: Create the Service Provider

The provider publishes the package views into the theme's views_path.

File: packages/Webkul/CustomTheme/src/Providers/CustomThemeServiceProvider.php

php
<?php

namespace Webkul\CustomTheme\Providers;

use Illuminate\Support\ServiceProvider;

class CustomThemeServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     */
    public function boot(): void
    {
        $this->publishes([
            __DIR__.'/../Resources/views' => resource_path('themes/custom-theme/views'),
        ], 'custom-theme-views');
    }
}

publishes() copies the views when you run vendor:publish. Skipping the Publish Step reads them straight from the package instead.

Step 2: Move the Views into the Package

Copy the theme folder's views directory into the package:

bash
cp -r resources/themes/custom-theme/views/. packages/Webkul/CustomTheme/src/Resources/views/

The home page is now at packages/Webkul/CustomTheme/src/Resources/views/home/index.blade.php; if you skipped the previous page, create it from Step 2 there. Package views follow the same rule as the theme folder: the path under Resources/views must match the Shop view's path.

Step 3: Add Translations

Later pages give the package strings of its own, such as section names and validation messages, in the custom-theme:: namespace.

File: packages/Webkul/CustomTheme/src/Resources/lang/en/app.php

php
<?php

return [
    'sections' => [
        'hero-banner' => 'Hero Banner',
        'slides' => 'Slides',
        'add-slide' => 'Add Slide',
        'image' => 'Image',
        'heading' => 'Heading',
        'deals-carousel' => 'Deals Carousel',
        'on-sale' => 'On Sale',
        'services' => 'Services',
        'free-shipping' => 'Free Shipping',
        'free-shipping-info' => 'Free shipping on all orders',
    ],

    'validation' => [
        'password' => 'Password',
        'strong-password' => 'Use at least 8 characters with an uppercase letter, a lowercase letter and a digit.',
    ],
];

Load the directory at the end of the provider's boot() method:

File: packages/Webkul/CustomTheme/src/Providers/CustomThemeServiceProvider.php

php
<?php

namespace Webkul\CustomTheme\Providers;

use Illuminate\Support\ServiceProvider;

class CustomThemeServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     */
    public function boot(): void
    {
        $this->publishes([
            __DIR__.'/../Resources/views' => resource_path('themes/custom-theme/views'),
        ], 'custom-theme-views');

        $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'custom-theme');
    }
}

trans('custom-theme::app.sections.hero-banner') now reads the file. Add the same keys for every locale the store serves; see Localization.

Step 4: Autoload and Register the Package

Add the namespace to the psr-4 map of the root composer.json, beside the Webkul\\ entries already there:

File: composer.json

json
"autoload": {
    "psr-4": {
        "Webkul\\CustomTheme\\": "packages/Webkul/CustomTheme/src"
    }
}

Regenerate the autoloader, or Laravel can't load the provider:

bash
composer dump-autoload

Then register the provider:

File: bootstrap/providers.php

php
<?php

use App\Providers\AppServiceProvider;
use Webkul\CustomTheme\Providers\CustomThemeServiceProvider;

return [
    AppServiceProvider::class,

    // ...

    CustomThemeServiceProvider::class,
];

Package Development covers package registration in depth.

Step 5: Publish the Views

Publish the package views into resources/themes/custom-theme/views, and clear the cache:

bash
php artisan vendor:publish --provider="Webkul\CustomTheme\Providers\CustomThemeServiceProvider" --force

php artisan optimize:clear

--force overwrites the published copies, so run it again after every change to a package view, and never edit the published copies.

Test It

  1. php artisan vendor:publish --provider="Webkul\CustomTheme\Providers\CustomThemeServiceProvider" --force copies packages/Webkul/CustomTheme/src/Resources/views to resources/themes/custom-theme/views. "No publishable resources" means the provider isn't autoloaded or registered (Step 4).
  2. php artisan tinker --execute="echo trans('custom-theme::app.sections.hero-banner');" prints Hero Banner.
  3. The home page of a channel running custom-theme shows your template.
  4. Change the package's home view, publish again with --force, and reload: the change shows.

Skipping the Publish Step

Publishing leaves two copies of every view. Instead, register the package views under the theme code in the provider's boot() method, in place of the publishes() call; when a theme other than default is active, that namespace is searched before the Shop package:

php
$this->loadViewsFrom(__DIR__.'/../Resources/views', 'custom-theme');

With custom-theme active, shop::home.index resolves to the package file directly. To use another namespace name, register it under that name and add 'views_namespace' => 'that-name' to the theme's entry.

  • Delete the published copies, such as resources/themes/custom-theme/views/home/index.blade.php, but keep the directory. views_path is searched first, so a stale copy keeps answering, and it stays the place where a store can override one file of your package.
  • Components still need views_path. The theme namespace isn't searched for Blade components, so an override of <x-shop::layouts> or any other component must be published there (keep a publishes() call for those files).
  • Not for the default theme. The shortcut is skipped when the active theme code is default.

The full order is in How Views Are Resolved.

Symlinking the Views

Republishing after every change is slow while you develop. Point the theme's views_path at the package views instead. Move any edits out of the published directory first, then run from the project root:

bash
rm -rf resources/themes/custom-theme/views

ln -s $(pwd)/packages/Webkul/CustomTheme/src/Resources/views resources/themes/custom-theme/views

Alternatively, iterate in resources/themes/custom-theme/views and copy the finished files into the package.

Things to Watch

  • views_path always wins. Published copies are searched before the package's own views, however those reach the finder.
  • Edit the package, not the published copy. vendor:publish --force replaces the published files.
  • The theme gallery shows only the theme's name. The screenshot, author, version and description come from the core file packages/Webkul/Theme/src/Resources/catalog.php, read by the protected Webkul\Theme\ThemeCatalog::catalogEntries(). To show yours, bind a subclass of ThemeCatalog whose catalogEntries() adds an entry under your theme code, with a remote screenshot URL, since any other path is read from the admin build.
  • Symbolic links need extra setup on Windows.

Next Step

The package still reuses the default theme's build. Next, give it its own Tailwind CSS 4 and Vite build.

Continue to: Vite-Powered Theme Assets

Released under the MIT License.