status: completed

input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/Theme.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/Theme.php
+<?php
+
+namespace App\Models;
+
+use App\Enums\ThemeStatus;
+use App\Models\Concerns\BelongsToStore;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\HasOne;
+
+class Theme extends Model
+{
+    /** @use HasFactory<\Database\Factories\ThemeFactory> */
+    use BelongsToStore, HasFactory;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'name',
+        'version',
+        'status',
+        'published_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'status' => 'draft',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    /**
+     * @return HasMany<ThemeFile, $this>
+     */
+    public function files(): HasMany
+    {
+        return $this->hasMany(ThemeFile::class)->orderBy('path');
+    }
+
+    /**
+     * @return HasOne<ThemeSettings, $this>
+     */
+    public function settings(): HasOne
+    {
+        return $this->hasOne(ThemeSettings::class);
+    }
+
+    public function isPublished(): bool
+    {
+        return $this->status === ThemeStatus::Published && $this->published_at !== null;
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'status' => ThemeStatus::class,
+            'published_at' => 'datetime',
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class ThemeFile extends Model
+{
+    /** @use HasFactory<\Database\Factories\ThemeFileFactory> */
+    use HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'theme_id',
+        'path',
+        'storage_key',
+        'sha256',
+        'byte_size',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'byte_size' => 0,
+    ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('theme', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+    }
+
+    /**
+     * @return BelongsTo<Theme, $this>
+     */
+    public function theme(): BelongsTo
+    {
+        return $this->belongsTo(Theme::class);
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'byte_size' => 'integer',
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class ThemeSettings extends Model
+{
+    /** @use HasFactory<\Database\Factories\ThemeSettingsFactory> */
+    use HasFactory;
+
+    public $incrementing = false;
+
+    public const CREATED_AT = null;
+
+    protected $primaryKey = 'theme_id';
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'theme_id',
+        'settings_json',
+    ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('theme', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+    }
+
+    /**
+     * @return BelongsTo<Theme, $this>
+     */
+    public function theme(): BelongsTo
+    {
+        return $this->belongsTo(Theme::class);
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'settings_json' => 'array',
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/Page.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/Page.php
+<?php
+
+namespace App\Models;
+
+use App\Enums\PageStatus;
+use App\Models\Concerns\BelongsToStore;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class Page extends Model
+{
+    /** @use HasFactory<\Database\Factories\PageFactory> */
+    use BelongsToStore, HasFactory;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'title',
+        'handle',
+        'body_html',
+        'status',
+        'published_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'status' => 'draft',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    public function isPublished(): bool
+    {
+        return $this->status === PageStatus::Published && $this->published_at !== null;
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'status' => PageStatus::class,
+            'published_at' => 'datetime',
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php
+<?php
+
+namespace App\Models;
+
+use App\Models\Concerns\BelongsToStore;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+
+class NavigationMenu extends Model
+{
+    /** @use HasFactory<\Database\Factories\NavigationMenuFactory> */
+    use BelongsToStore, HasFactory;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'handle',
+        'title',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    /**
+     * @return HasMany<NavigationItem, $this>
+     */
+    public function items(): HasMany
+    {
+        return $this->hasMany(NavigationItem::class, 'menu_id')->orderBy('position');
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php
+<?php
+
+namespace App\Models;
+
+use App\Enums\NavigationItemType;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class NavigationItem extends Model
+{
+    /** @use HasFactory<\Database\Factories\NavigationItemFactory> */
+    use HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'menu_id',
+        'type',
+        'label',
+        'url',
+        'resource_id',
+        'position',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'type' => 'link',
+        'position' => 0,
+    ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope('current_store', function (Builder $builder): void {
+            if (! app()->bound('current_store')) {
+                return;
+            }
+
+            $store = app('current_store');
+
+            if (! $store instanceof Store) {
+                return;
+            }
+
+            $builder->whereHas('menu', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+    }
+
+    /**
+     * @return BelongsTo<NavigationMenu, $this>
+     */
+    public function menu(): BelongsTo
+    {
+        return $this->belongsTo(NavigationMenu::class, 'menu_id');
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'type' => NavigationItemType::class,
+            'resource_id' => 'integer',
+            'position' => 'integer',
+        ];
+    }
+}
*** End Patch


stdout:
Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/app/Models/Theme.php
A /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php
A /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php
A /Users/fabianwesner/Herd/shop/app/Models/Page.php
A /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php
A /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php
D /Users/fabianwesner/Herd/shop/app/Models/Theme.php
D /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php
D /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php
D /Users/fabianwesner/Herd/shop/app/Models/Page.php
D /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php
D /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php

type: add

content:
<?php

namespace App\Models;

use App\Enums\ThemeStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Theme extends Model
{
    /** @use HasFactory<\Database\Factories\ThemeFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'name',
        'version',
        'status',
        'published_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'draft',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return HasMany<ThemeFile, $this>
     */
    public function files(): HasMany
    {
        return $this->hasMany(ThemeFile::class)->orderBy('path');
    }

    /**
     * @return HasOne<ThemeSettings, $this>
     */
    public function settings(): HasOne
    {
        return $this->hasOne(ThemeSettings::class);
    }

    public function isPublished(): bool
    {
        return $this->status === ThemeStatus::Published && $this->published_at !== null;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => ThemeStatus::class,
            'published_at' => 'datetime',
        ];
    }
}

content:
<?php

namespace App\Models;

use App\Enums\PageStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Page extends Model
{
    /** @use HasFactory<\Database\Factories\PageFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'title',
        'handle',
        'body_html',
        'status',
        'published_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'draft',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    public function isPublished(): bool
    {
        return $this->status === PageStatus::Published && $this->published_at !== null;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => PageStatus::class,
            'published_at' => 'datetime',
        ];
    }
}

content:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ThemeSettings extends Model
{
    /** @use HasFactory<\Database\Factories\ThemeSettingsFactory> */
    use HasFactory;

    public $incrementing = false;

    public const CREATED_AT = null;

    protected $primaryKey = 'theme_id';

    /**
     * @var list<string>
     */
    protected $fillable = [
        'theme_id',
        'settings_json',
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('theme', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<Theme, $this>
     */
    public function theme(): BelongsTo
    {
        return $this->belongsTo(Theme::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'settings_json' => 'array',
        ];
    }
}

content:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ThemeFile extends Model
{
    /** @use HasFactory<\Database\Factories\ThemeFileFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'theme_id',
        'path',
        'storage_key',
        'sha256',
        'byte_size',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'byte_size' => 0,
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('theme', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<Theme, $this>
     */
    public function theme(): BelongsTo
    {
        return $this->belongsTo(Theme::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'byte_size' => 'integer',
        ];
    }
}

content:
<?php

namespace App\Models;

use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class NavigationMenu extends Model
{
    /** @use HasFactory<\Database\Factories\NavigationMenuFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'handle',
        'title',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return HasMany<NavigationItem, $this>
     */
    public function items(): HasMany
    {
        return $this->hasMany(NavigationItem::class, 'menu_id')->orderBy('position');
    }
}

content:
<?php

namespace App\Models;

use App\Enums\NavigationItemType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class NavigationItem extends Model
{
    /** @use HasFactory<\Database\Factories\NavigationItemFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'menu_id',
        'type',
        'label',
        'url',
        'resource_id',
        'position',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'link',
        'position' => 0,
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('menu', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<NavigationMenu, $this>
     */
    public function menu(): BelongsTo
    {
        return $this->belongsTo(NavigationMenu::class, 'menu_id');
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => NavigationItemType::class,
            'resource_id' => 'integer',
            'position' => 'integer',
        ];
    }
}

status: completed


output: {"output":"Success. Updated the following files:\nA /Users/fabianwesner/Herd/shop/app/Models/Theme.php\nA /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php\nA /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php\nA /Users/fabianwesner/Herd/shop/app/Models/Page.php\nA /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php\nA /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php\nD /Users/fabianwesner/Herd/shop/app/Models/Theme.php\nD /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php\nD /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php\nD /Users/fabianwesner/Herd/shop/app/Models/Page.php\nD /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php\nD /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/Store.php
@@
     public function settings(): HasOne
     {
         return $this->hasOne(StoreSettings::class);
     }
+
+    /**
+     * @return HasMany<Theme, $this>
+     */
+    public function themes(): HasMany
+    {
+        return $this->hasMany(Theme::class);
+    }
+
+    /**
+     * @return HasMany<Page, $this>
+     */
+    public function pages(): HasMany
+    {
+        return $this->hasMany(Page::class);
+    }
+
+    /**
+     * @return HasMany<NavigationMenu, $this>
+     */
+    public function navigationMenus(): HasMany
+    {
+        return $this->hasMany(NavigationMenu::class);
+    }
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Models/Store.php

type: update

unified_diff:
@@ -64,2 +64,26 @@

+    /**
+     * @return HasMany<Theme, $this>
+     */
+    public function themes(): HasMany
+    {
+        return $this->hasMany(Theme::class);
+    }
+
+    /**
+     * @return HasMany<Page, $this>
+     */
+    public function pages(): HasMany
+    {
+        return $this->hasMany(Page::class);
+    }
+
+    /**
+     * @return HasMany<NavigationMenu, $this>
+     */
+    public function navigationMenus(): HasMany
+    {
+        return $this->hasMany(NavigationMenu::class);
+    }
+
     public function isSuspended(): bool


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Models/Store.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/ThemeFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/ThemeFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Enums\ThemeStatus;
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Theme>
+ */
+class ThemeFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'store_id' => Store::factory(),
+            'name' => fake()->words(2, true).' theme',
+            'version' => '1.0.0',
+            'status' => ThemeStatus::Draft,
+            'published_at' => null,
+        ];
+    }
+
+    public function published(): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'status' => ThemeStatus::Published,
+            'published_at' => now(),
+        ]);
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/ThemeFileFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/ThemeFileFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Models\Theme;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ThemeFile>
+ */
+class ThemeFileFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        $path = 'sections/'.fake()->unique()->slug().'.blade.php';
+
+        return [
+            'theme_id' => Theme::factory(),
+            'path' => $path,
+            'storage_key' => 'themes/'.Str::random(12).'/'.$path,
+            'sha256' => hash('sha256', $path.fake()->uuid()),
+            'byte_size' => fake()->numberBetween(128, 4096),
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/ThemeSettingsFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/ThemeSettingsFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Models\Theme;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ThemeSettings>
+ */
+class ThemeSettingsFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'theme_id' => Theme::factory(),
+            'settings_json' => [
+                'announcement' => [
+                    'enabled' => true,
+                    'text' => 'Free shipping on orders over 75.00 EUR',
+                    'url' => null,
+                ],
+                'header' => [
+                    'sticky' => true,
+                    'main_menu' => 'main-menu',
+                ],
+                'footer' => [
+                    'menu' => 'footer-menu',
+                    'tagline' => 'A self-contained demo storefront.',
+                ],
+            ],
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/PageFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/PageFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Enums\PageStatus;
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Page>
+ */
+class PageFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        $title = Str::title(fake()->words(2, true));
+
+        return [
+            'store_id' => Store::factory(),
+            'title' => $title,
+            'handle' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
+            'body_html' => '<p>'.fake()->sentence().'</p>',
+            'status' => PageStatus::Draft,
+            'published_at' => null,
+        ];
+    }
+
+    public function published(): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'status' => PageStatus::Published,
+            'published_at' => now(),
+        ]);
+    }
+
+    public function archived(): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'status' => PageStatus::Archived,
+            'published_at' => now()->subDay(),
+        ]);
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/NavigationMenuFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/NavigationMenuFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\NavigationMenu>
+ */
+class NavigationMenuFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        $title = Str::title(fake()->words(2, true));
+
+        return [
+            'store_id' => Store::factory(),
+            'handle' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
+            'title' => $title,
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/NavigationItemFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/NavigationItemFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Enums\NavigationItemType;
+use App\Models\NavigationMenu;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\NavigationItem>
+ */
+class NavigationItemFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'menu_id' => NavigationMenu::factory(),
+            'type' => NavigationItemType::Link,
+            'label' => fake()->words(2, true),
+            'url' => '/'.fake()->slug(),
+            'resource_id' => null,
+            'position' => 0,
+        ];
+    }
+
+    public function page(int $pageId): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'type' => NavigationItemType::Page,
+            'url' => null,
+            'resource_id' => $pageId,
+        ]);
+    }
+
+    public function collection(int $collectionId): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'type' => NavigationItemType::Collection,
+            'url' => null,
+            'resource_id' => $collectionId,
+        ]);
+    }
+
+    public function product(int $productId): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'type' => NavigationItemType::Product,
+            'url' => null,
+            'resource_id' => $productId,
+        ]);
+    }
+}
*** End Patch


stdout:
Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/database/factories/ThemeFactory.php
A /Users/fabianwesner/Herd/shop/database/factories/ThemeFileFactory.php
A /Users/fabianwesner/Herd/shop/database/factories/ThemeSettingsFactory.php
A /Users/fabianwesner/Herd/shop/database/factories/PageFactory.php
A /Users/fabianwesner/Herd/shop/database/factories/NavigationMenuFactory.php
A /Users/fabianwesner/Herd/shop/database/factories/NavigationItemFactory.php
D /Users/fabianwesner/Herd/shop/database/factories/ThemeFactory.php
D /Users/fabianwesner/Herd/shop/database/factories/ThemeFileFactory.php
D /Users/fabianwesner/Herd/shop/database/factories/ThemeSettingsFactory.php
D /Users/fabianwesner/Herd/shop/database/factories/PageFactory.php
D /Users/fabianwesner/Herd/shop/database/factories/NavigationMenuFactory.php
D /Users/fabianwesner/Herd/shop/database/factories/NavigationItemFactory.php

content:
<?php

namespace Database\Factories;

use App\Enums\NavigationItemType;
use App\Models\NavigationMenu;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\NavigationItem>
 */
class NavigationItemFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'menu_id' => NavigationMenu::factory(),
            'type' => NavigationItemType::Link,
            'label' => fake()->words(2, true),
            'url' => '/'.fake()->slug(),
            'resource_id' => null,
            'position' => 0,
        ];
    }

    public function page(int $pageId): static
    {
        return $this->state(fn (array $attributes): array => [
            'type' => NavigationItemType::Page,
            'url' => null,
            'resource_id' => $pageId,
        ]);
    }

    public function collection(int $collectionId): static
    {
        return $this->state(fn (array $attributes): array => [
            'type' => NavigationItemType::Collection,
            'url' => null,
            'resource_id' => $collectionId,
        ]);
    }

    public function product(int $productId): static
    {
        return $this->state(fn (array $attributes): array => [
            'type' => NavigationItemType::Product,
            'url' => null,
            'resource_id' => $productId,
        ]);
    }
}

content:
<?php

namespace Database\Factories;

use App\Enums\PageStatus;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Page>
 */
class PageFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $title = Str::title(fake()->words(2, true));

        return [
            'store_id' => Store::factory(),
            'title' => $title,
            'handle' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
            'body_html' => '<p>'.fake()->sentence().'</p>',
            'status' => PageStatus::Draft,
            'published_at' => null,
        ];
    }

    public function published(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => PageStatus::Published,
            'published_at' => now(),
        ]);
    }

    public function archived(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => PageStatus::Archived,
            'published_at' => now()->subDay(),
        ]);
    }
}

content:
<?php

namespace Database\Factories;

use App\Models\Theme;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ThemeFile>
 */
class ThemeFileFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $path = 'sections/'.fake()->unique()->slug().'.blade.php';

        return [
            'theme_id' => Theme::factory(),
            'path' => $path,
            'storage_key' => 'themes/'.Str::random(12).'/'.$path,
            'sha256' => hash('sha256', $path.fake()->uuid()),
            'byte_size' => fake()->numberBetween(128, 4096),
        ];
    }
}

content:
<?php

namespace Database\Factories;

use App\Enums\ThemeStatus;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Theme>
 */
class ThemeFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'store_id' => Store::factory(),
            'name' => fake()->words(2, true).' theme',
            'version' => '1.0.0',
            'status' => ThemeStatus::Draft,
            'published_at' => null,
        ];
    }

    public function published(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => ThemeStatus::Published,
            'published_at' => now(),
        ]);
    }
}

content:
<?php

namespace Database\Factories;

use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\NavigationMenu>
 */
class NavigationMenuFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $title = Str::title(fake()->words(2, true));

        return [
            'store_id' => Store::factory(),
            'handle' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
            'title' => $title,
        ];
    }
}

content:
<?php

namespace Database\Factories;

use App\Models\Theme;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ThemeSettings>
 */
class ThemeSettingsFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'theme_id' => Theme::factory(),
            'settings_json' => [
                'announcement' => [
                    'enabled' => true,
                    'text' => 'Free shipping on orders over 75.00 EUR',
                    'url' => null,
                ],
                'header' => [
                    'sticky' => true,
                    'main_menu' => 'main-menu',
                ],
                'footer' => [
                    'menu' => 'footer-menu',
                    'tagline' => 'A self-contained demo storefront.',
                ],
            ],
        ];
    }
}


status: completed

input:
*** Begin Patch
*** Add File: /Users/fabianwesner/Herd/shop/app/Services/ThemeSettingsService.php
+<?php
+
+namespace App\Services;
+
+use App\Enums\ThemeStatus;
+use App\Models\Store;
+use App\Models\Theme;
+use Illuminate\Support\Facades\Cache;
+
+class ThemeSettingsService
+{
+    /**
+     * @return array<string, mixed>
+     */
+    public function forStore(Store $store): array
+    {
+        return Cache::remember($this->cacheKey($store), now()->addMinutes(5), function () use ($store): array {
+            $theme = $this->publishedTheme($store);
+
+            if ($theme === null) {
+                return $this->defaultsForStore($store);
+            }
+
+            $settings = $theme->settings?->settings_json ?? [];
+
+            return array_replace_recursive($this->defaultsForStore($store), $settings);
+        });
+    }
+
+    public function publishedTheme(Store $store): ?Theme
+    {
+        return Theme::withoutGlobalScopes()
+            ->with('settings')
+            ->where('store_id', $store->getKey())
+            ->where('status', ThemeStatus::Published)
+            ->whereNotNull('published_at')
+            ->latest('published_at')
+            ->first();
+    }
+
+    public function forget(Store $store): void
+    {
+        Cache::forget($this->cacheKey($store));
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    public function defaultsForStore(Store $store): array
+    {
+        return [
+            'announcement' => [
+                'enabled' => true,
+                'text' => "Free shipping on orders over 75.00 {$store->default_currency}",
+                'url' => null,
+            ],
+            'header' => [
+                'sticky' => true,
+                'main_menu' => 'main-menu',
+            ],
+            'footer' => [
+                'menu' => 'footer-menu',
+                'tagline' => 'A self-contained demo storefront with scoped catalog data and checkout-ready products.',
+            ],
+            'home' => [
+                'hero' => [
+                    'eyebrow' => 'New season essentials',
+                    'heading' => $store->name,
+                    'subheading' => 'Browse a scoped demo catalog with variants, inventory states, sale pricing, and digital products.',
+                    'primary_label' => 'Shop new arrivals',
+                    'primary_url' => '/collections/new-arrivals',
+                    'secondary_label' => 'View collections',
+                    'secondary_url' => '/collections',
+                ],
+                'featured_product_limit' => 8,
+                'featured_collection_limit' => 4,
+            ],
+        ];
+    }
+
+    private function cacheKey(Store $store): string
+    {
+        return "theme_settings:{$store->getKey()}";
+    }
+}
*** Add File: /Users/fabianwesner/Herd/shop/app/Services/NavigationService.php
+<?php
+
+namespace App\Services;
+
+use App\Enums\NavigationItemType;
+use App\Models\Collection as ProductCollection;
+use App\Models\NavigationItem;
+use App\Models\NavigationMenu;
+use App\Models\Page;
+use App\Models\Product;
+use App\Models\Store;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Str;
+
+class NavigationService
+{
+    /**
+     * @return array<int, array{label: string, url: string, type: string, external: bool, children: array<int, mixed>}>
+     */
+    public function forHandle(Store $store, string $handle): array
+    {
+        $menu = NavigationMenu::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->where('handle', $handle)
+            ->first();
+
+        if ($menu === null) {
+            return [];
+        }
+
+        return $this->buildTree($menu);
+    }
+
+    /**
+     * @return array<int, array{label: string, url: string, type: string, external: bool, children: array<int, mixed>}>
+     */
+    public function buildTree(NavigationMenu $menu): array
+    {
+        return Cache::remember($this->cacheKey($menu), now()->addMinutes(5), function () use ($menu): array {
+            return $menu->items()
+                ->with('menu')
+                ->get()
+                ->map(fn (NavigationItem $item): array => [
+                    'label' => $item->label,
+                    'url' => $this->resolveUrl($item),
+                    'type' => $item->type->value,
+                    'external' => $this->isExternal((string) ($item->url ?? '')),
+                    'children' => [],
+                ])
+                ->all();
+        });
+    }
+
+    public function resolveUrl(NavigationItem $item): string
+    {
+        return match ($item->type) {
+            NavigationItemType::Link => $item->url ?: '#',
+            NavigationItemType::Page => $this->pageUrl($item),
+            NavigationItemType::Collection => $this->collectionUrl($item),
+            NavigationItemType::Product => $this->productUrl($item),
+        };
+    }
+
+    public function forget(NavigationMenu $menu): void
+    {
+        Cache::forget($this->cacheKey($menu));
+    }
+
+    private function pageUrl(NavigationItem $item): string
+    {
+        $page = Page::withoutGlobalScopes()
+            ->where('store_id', $this->storeIdFor($item))
+            ->find($item->resource_id);
+
+        return $page ? route('pages.show', $page->handle, false) : '#';
+    }
+
+    private function collectionUrl(NavigationItem $item): string
+    {
+        $collection = ProductCollection::withoutGlobalScopes()
+            ->where('store_id', $this->storeIdFor($item))
+            ->find($item->resource_id);
+
+        return $collection ? route('collections.show', $collection->handle, false) : '#';
+    }
+
+    private function productUrl(NavigationItem $item): string
+    {
+        $product = Product::withoutGlobalScopes()
+            ->where('store_id', $this->storeIdFor($item))
+            ->find($item->resource_id);
+
+        return $product ? route('products.show', $product->handle, false) : '#';
+    }
+
+    private function storeIdFor(NavigationItem $item): ?int
+    {
+        if ($item->relationLoaded('menu') && $item->menu !== null) {
+            return (int) $item->menu->store_id;
+        }
+
+        return NavigationMenu::withoutGlobalScopes()
+            ->whereKey($item->menu_id)
+            ->value('store_id');
+    }
+
+    private function isExternal(string $url): bool
+    {
+        return Str::startsWith($url, ['http://', 'https://', 'mailto:', 'tel:']);
+    }
+
+    private function cacheKey(NavigationMenu $menu): string
+    {
+        return "navigation:{$menu->store_id}:{$menu->handle}";
+    }
+}
*** End Patch


stdout:
Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/app/Services/ThemeSettingsService.php
A /Users/fabianwesner/Herd/shop/app/Services/NavigationService.php

content:
<?php

namespace App\Services;

use App\Enums\ThemeStatus;
use App\Models\Store;
use App\Models\Theme;
use Illuminate\Support\Facades\Cache;

class ThemeSettingsService
{
    /**
     * @return array<string, mixed>
     */
    public function forStore(Store $store): array
    {
        return Cache::remember($this->cacheKey($store), now()->addMinutes(5), function () use ($store): array {
            $theme = $this->publishedTheme($store);

            if ($theme === null) {
                return $this->defaultsForStore($store);
            }

            $settings = $theme->settings?->settings_json ?? [];

            return array_replace_recursive($this->defaultsForStore($store), $settings);
        });
    }

    public function publishedTheme(Store $store): ?Theme
    {
        return Theme::withoutGlobalScopes()
            ->with('settings')
            ->where('store_id', $store->getKey())
            ->where('status', ThemeStatus::Published)
            ->whereNotNull('published_at')
            ->latest('published_at')
            ->first();
    }

    public function forget(Store $store): void
    {
        Cache::forget($this->cacheKey($store));
    }

    /**
     * @return array<string, mixed>
     */
    public function defaultsForStore(Store $store): array
    {
        return [
            'announcement' => [
                'enabled' => true,
                'text' => "Free shipping on orders over 75.00 {$store->default_currency}",
                'url' => null,
            ],
            'header' => [
                'sticky' => true,
                'main_menu' => 'main-menu',
            ],
            'footer' => [
                'menu' => 'footer-menu',
                'tagline' => 'A self-contained demo storefront with scoped catalog data and checkout-ready products.',
            ],
            'home' => [
                'hero' => [
                    'eyebrow' => 'New season essentials',
                    'heading' => $store->name,
                    'subheading' => 'Browse a scoped demo catalog with variants, inventory states, sale pricing, and digital products.',
                    'primary_label' => 'Shop new arrivals',
                    'primary_url' => '/collections/new-arrivals',
                    'secondary_label' => 'View collections',
                    'secondary_url' => '/collections',
                ],
                'featured_product_limit' => 8,
                'featured_collection_limit' => 4,
            ],
        ];
    }

    private function cacheKey(Store $store): string
    {
        return "theme_settings:{$store->getKey()}";
    }
}

content:
<?php

namespace App\Services;

use App\Enums\NavigationItemType;
use App\Models\Collection as ProductCollection;
use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Product;
use App\Models\Store;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class NavigationService
{
    /**
     * @return array<int, array{label: string, url: string, type: string, external: bool, children: array<int, mixed>}>
     */
    public function forHandle(Store $store, string $handle): array
    {
        $menu = NavigationMenu::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('handle', $handle)
            ->first();

        if ($menu === null) {
            return [];
        }

        return $this->buildTree($menu);
    }

    /**
     * @return array<int, array{label: string, url: string, type: string, external: bool, children: array<int, mixed>}>
     */
    public function buildTree(NavigationMenu $menu): array
    {
        return Cache::remember($this->cacheKey($menu), now()->addMinutes(5), function () use ($menu): array {
            return $menu->items()
                ->with('menu')
                ->get()
                ->map(fn (NavigationItem $item): array => [
                    'label' => $item->label,
                    'url' => $this->resolveUrl($item),
                    'type' => $item->type->value,
                    'external' => $this->isExternal((string) ($item->url ?? '')),
                    'children' => [],
                ])
                ->all();
        });
    }

    public function resolveUrl(NavigationItem $item): string
    {
        return match ($item->type) {
            NavigationItemType::Link => $item->url ?: '#',
            NavigationItemType::Page => $this->pageUrl($item),
            NavigationItemType::Collection => $this->collectionUrl($item),
            NavigationItemType::Product => $this->productUrl($item),
        };
    }

    public function forget(NavigationMenu $menu): void
    {
        Cache::forget($this->cacheKey($menu));
    }

    private function pageUrl(NavigationItem $item): string
    {
        $page = Page::withoutGlobalScopes()
            ->where('store_id', $this->storeIdFor($item))
            ->find($item->resource_id);

        return $page ? route('pages.show', $page->handle, false) : '#';
    }

    private function collectionUrl(NavigationItem $item): string
    {
        $collection = ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeIdFor($item))
            ->find($item->resource_id);

        return $collection ? route('collections.show', $collection->handle, false) : '#';
    }

    private function productUrl(NavigationItem $item): string
    {
        $product = Product::withoutGlobalScopes()
            ->where('store_id', $this->storeIdFor($item))
            ->find($item->resource_id);

        return $product ? route('products.show', $product->handle, false) : '#';
    }

    private function storeIdFor(NavigationItem $item): ?int
    {
        if ($item->relationLoaded('menu') && $item->menu !== null) {
            return (int) $item->menu->store_id;
        }

        return NavigationMenu::withoutGlobalScopes()
            ->whereKey($item->menu_id)
            ->value('store_id');
    }

    private function isExternal(string $url): bool
    {
        return Str::startsWith($url, ['http://', 'https://', 'mailto:', 'tel:']);
    }

    private function cacheKey(NavigationMenu $menu): string
    {
        return "navigation:{$menu->store_id}:{$menu->handle}";
    }
}


output: {"output":"Success. Updated the following files:\nA /Users/fabianwesner/Herd/shop/app/Services/ThemeSettingsService.php\nA /Users/fabianwesner/Herd/shop/app/Services/NavigationService.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/database/seeders/ThemeSeeder.php
*** Add File: /Users/fabianwesner/Herd/shop/database/seeders/ThemeSeeder.php
+<?php
+
+namespace Database\Seeders;
+
+use App\Models\Store;
+use App\Models\Theme;
+use Illuminate\Database\Seeder;
+
+class ThemeSeeder extends Seeder
+{
+    /**
+     * Run the database seeds.
+     */
+    public function run(): void
+    {
+        Store::query()->orderBy('id')->get()->each(function (Store $store): void {
+            Theme::withoutGlobalScopes()->updateOrCreate(
+                [
+                    'store_id' => $store->getKey(),
+                    'name' => "{$store->name} Default",
+                ],
+                [
+                    'version' => '1.0.0',
+                    'status' => 'published',
+                    'published_at' => now(),
+                ],
+            );
+        });
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/seeders/ThemeFileSeeder.php
*** Add File: /Users/fabianwesner/Herd/shop/database/seeders/ThemeFileSeeder.php
+<?php
+
+namespace Database\Seeders;
+
+use App\Models\Theme;
+use App\Models\ThemeFile;
+use Illuminate\Database\Seeder;
+
+class ThemeFileSeeder extends Seeder
+{
+    /**
+     * Run the database seeds.
+     */
+    public function run(): void
+    {
+        Theme::withoutGlobalScopes()->get()->each(function (Theme $theme): void {
+            foreach ([
+                'layouts/storefront.blade.php',
+                'sections/hero.blade.php',
+                'sections/featured-products.blade.php',
+            ] as $path) {
+                ThemeFile::withoutGlobalScopes()->updateOrCreate(
+                    [
+                        'theme_id' => $theme->getKey(),
+                        'path' => $path,
+                    ],
+                    [
+                        'storage_key' => "themes/{$theme->getKey()}/{$path}",
+                        'sha256' => hash('sha256', "{$theme->getKey()}:{$path}"),
+                        'byte_size' => 1024,
+                    ],
+                );
+            }
+        });
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/seeders/ThemeSettingsSeeder.php
*** Add File: /Users/fabianwesner/Herd/shop/database/seeders/ThemeSettingsSeeder.php
+<?php
+
+namespace Database\Seeders;
+
+use App\Models\Theme;
+use App\Models\ThemeSettings;
+use App\Services\ThemeSettingsService;
+use Illuminate\Database\Seeder;
+
+class ThemeSettingsSeeder extends Seeder
+{
+    /**
+     * Run the database seeds.
+     */
+    public function run(): void
+    {
+        Theme::withoutGlobalScopes()
+            ->with('store')
+            ->get()
+            ->each(function (Theme $theme): void {
+                ThemeSettings::withoutGlobalScopes()->updateOrCreate(
+                    ['theme_id' => $theme->getKey()],
+                    [
+                        'settings_json' => app(ThemeSettingsService::class)->defaultsForStore($theme->store),
+                        'updated_at' => now(),
+                    ],
+                );
+            });
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/seeders/PageSeeder.php
*** Add File: /Users/fabianwesner/Herd/shop/database/seeders/PageSeeder.php
+<?php
+
+namespace Database\Seeders;
+
+use App\Models\Page;
+use App\Models\Store;
+use Illuminate\Database\Seeder;
+
+class PageSeeder extends Seeder
+{
+    /**
+     * Run the database seeds.
+     */
+    public function run(): void
+    {
+        Store::query()->get()->each(function (Store $store): void {
+            foreach ($this->pagesFor($store) as $page) {
+                Page::withoutGlobalScopes()->updateOrCreate(
+                    [
+                        'store_id' => $store->getKey(),
+                        'handle' => $page['handle'],
+                    ],
+                    [
+                        'title' => $page['title'],
+                        'body_html' => $page['body_html'],
+                        'status' => 'published',
+                        'published_at' => now(),
+                    ],
+                );
+            }
+        });
+    }
+
+    /**
+     * @return array<int, array{handle: string, title: string, body_html: string}>
+     */
+    private function pagesFor(Store $store): array
+    {
+        return [
+            [
+                'handle' => 'about',
+                'title' => 'About',
+                'body_html' => "
{$store->name} is the demo storefront for this self-contained shop platform.

The catalog includes multi-variant products, sale pricing, inventory rules, backorder handling, and digital product examples for checkout and storefront testing.
",
+            ],
+            [
+                'handle' => 'faq',
+                'title' => 'FAQ',
+                'body_html' => '
Shipping, payment, returns, and account flows are implemented progressively according to the project roadmap.
',
+            ],
+            [
+                'handle' => 'shipping',
+                'title' => 'Shipping',
+                'body_html' => '
Shipping zones, rates, and tax-aware checkout calculations are seeded in the checkout phase.
',
+            ],
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/seeders/NavigationMenuSeeder.php
*** Add File: /Users/fabianwesner/Herd/shop/database/seeders/NavigationMenuSeeder.php
+<?php
+
+namespace Database\Seeders;
+
+use App\Models\NavigationMenu;
+use App\Models\Store;
+use Illuminate\Database\Seeder;
+
+class NavigationMenuSeeder extends Seeder
+{
+    /**
+     * Run the database seeds.
+     */
+    public function run(): void
+    {
+        Store::query()->get()->each(function (Store $store): void {
+            foreach ([
+                'main-menu' => 'Main Menu',
+                'footer-menu' => 'Footer Menu',
+            ] as $handle => $title) {
+                NavigationMenu::withoutGlobalScopes()->updateOrCreate(
+                    [
+                        'store_id' => $store->getKey(),
+                        'handle' => $handle,
+                    ],
+                    ['title' => $title],
+                );
+            }
+        });
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/seeders/NavigationItemSeeder.php
*** Add File: /Users/fabianwesner/Herd/shop/database/seeders/NavigationItemSeeder.php
+<?php
+
+namespace Database\Seeders;
+
+use App\Models\Collection as ProductCollection;
+use App\Models\NavigationItem;
+use App\Models\NavigationMenu;
+use App\Models\Page;
+use App\Models\Store;
+use Illuminate\Database\Seeder;
+
+class NavigationItemSeeder extends Seeder
+{
+    /**
+     * Run the database seeds.
+     */
+    public function run(): void
+    {
+        Store::query()->get()->each(function (Store $store): void {
+            $menus = NavigationMenu::withoutGlobalScopes()
+                ->where('store_id', $store->getKey())
+                ->get()
+                ->keyBy('handle');
+
+            $this->replaceItems($menus->get('main-menu'), $this->mainMenuItems($store));
+            $this->replaceItems($menus->get('footer-menu'), $this->footerMenuItems($store));
+        });
+    }
+
+    /**
+     * @param  array<int, array{label: string, type: string, url?: string|null, resource_id?: int|null}>  $items
+     */
+    private function replaceItems(?NavigationMenu $menu, array $items): void
+    {
+        if ($menu === null) {
+            return;
+        }
+
+        NavigationItem::withoutGlobalScopes()
+            ->where('menu_id', $menu->getKey())
+            ->delete();
+
+        foreach ($items as $position => $item) {
+            NavigationItem::withoutGlobalScopes()->create([
+                'menu_id' => $menu->getKey(),
+                'type' => $item['type'],
+                'label' => $item['label'],
+                'url' => $item['url'] ?? null,
+                'resource_id' => $item['resource_id'] ?? null,
+                'position' => $position,
+            ]);
+        }
+    }
+
+    /**
+     * @return array<int, array{label: string, type: string, url?: string|null, resource_id?: int|null}>
+     */
+    private function mainMenuItems(Store $store): array
+    {
+        $newArrivals = $this->collection($store, 'new-arrivals') ?? $this->collection($store, 'featured');
+        $secondaryCollection = $this->collection($store, 't-shirts') ?? $this->collection($store, 'accessories');
+        $about = $this->page($store, 'about');
+
+        return array_values(array_filter([
+            ['label' => 'Collections', 'type' => 'link', 'url' => '/collections'],
+            $newArrivals ? ['label' => $newArrivals->title, 'type' => 'collection', 'resource_id' => $newArrivals->getKey()] : null,
+            $secondaryCollection ? ['label' => $secondaryCollection->title, 'type' => 'collection', 'resource_id' => $secondaryCollection->getKey()] : null,
+            ['label' => 'Search', 'type' => 'link', 'url' => '/search'],
+            $about ? ['label' => 'About', 'type' => 'page', 'resource_id' => $about->getKey()] : null,
+        ]));
+    }
+
+    /**
+     * @return array<int, array{label: string, type: string, url?: string|null, resource_id?: int|null}>
+     */
+    private function footerMenuItems(Store $store): array
+    {
+        $sale = $this->collection($store, 'sale');
+        $about = $this->page($store, 'about');
+        $faq = $this->page($store, 'faq');
+
+        return array_values(array_filter([
+            $sale ? ['label' => 'Sale', 'type' => 'collection', 'resource_id' => $sale->getKey()] : null,
+            $about ? ['label' => 'About', 'type' => 'page', 'resource_id' => $about->getKey()] : null,
+            $faq ? ['label' => 'FAQ', 'type' => 'page', 'resource_id' => $faq->getKey()] : null,
+            ['label' => 'Account', 'type' => 'link', 'url' => '/account'],
+        ]));
+    }
+
+    private function collection(Store $store, string $handle): ?ProductCollection
+    {
+        return ProductCollection::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->where('handle', $handle)
+            ->first();
+    }
+
+    private function page(Store $store, string $handle): ?Page
+    {
+        return Page::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->where('handle', $handle)
+            ->first();
+    }
+}
*** Update File: /Users/fabianwesner/Herd/shop/database/seeders/DatabaseSeeder.php
@@
             StoreSettingsSeeder::class,
             CollectionSeeder::class,
             ProductSeeder::class,
+            ThemeSeeder::class,
+            ThemeFileSeeder::class,
+            ThemeSettingsSeeder::class,
+            PageSeeder::class,
+            NavigationMenuSeeder::class,
+            NavigationItemSeeder::class,
             CustomerSeeder::class,
         ]);
     }
 }
*** End Patch


stdout:
Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/database/seeders/ThemeSeeder.php
A /Users/fabianwesner/Herd/shop/database/seeders/ThemeFileSeeder.php
A /Users/fabianwesner/Herd/shop/database/seeders/ThemeSettingsSeeder.php
A /Users/fabianwesner/Herd/shop/database/seeders/PageSeeder.php
A /Users/fabianwesner/Herd/shop/database/seeders/NavigationMenuSeeder.php
A /Users/fabianwesner/Herd/shop/database/seeders/NavigationItemSeeder.php
M /Users/fabianwesner/Herd/shop/database/seeders/DatabaseSeeder.php
D /Users/fabianwesner/Herd/shop/database/seeders/ThemeSeeder.php
D /Users/fabianwesner/Herd/shop/database/seeders/ThemeFileSeeder.php
D /Users/fabianwesner/Herd/shop/database/seeders/ThemeSettingsSeeder.php
D /Users/fabianwesner/Herd/shop/database/seeders/PageSeeder.php
D /Users/fabianwesner/Herd/shop/database/seeders/NavigationMenuSeeder.php
D /Users/fabianwesner/Herd/shop/database/seeders/NavigationItemSeeder.php

content:
<?php

namespace Database\Seeders;

use App\Models\Collection as ProductCollection;
use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Store;
use Illuminate\Database\Seeder;

class NavigationItemSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Store::query()->get()->each(function (Store $store): void {
            $menus = NavigationMenu::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->get()
                ->keyBy('handle');

            $this->replaceItems($menus->get('main-menu'), $this->mainMenuItems($store));
            $this->replaceItems($menus->get('footer-menu'), $this->footerMenuItems($store));
        });
    }

    /**
     * @param  array<int, array{label: string, type: string, url?: string|null, resource_id?: int|null}>  $items
     */
    private function replaceItems(?NavigationMenu $menu, array $items): void
    {
        if ($menu === null) {
            return;
        }

        NavigationItem::withoutGlobalScopes()
            ->where('menu_id', $menu->getKey())
            ->delete();

        foreach ($items as $position => $item) {
            NavigationItem::withoutGlobalScopes()->create([
                'menu_id' => $menu->getKey(),
                'type' => $item['type'],
                'label' => $item['label'],
                'url' => $item['url'] ?? null,
                'resource_id' => $item['resource_id'] ?? null,
                'position' => $position,
            ]);
        }
    }

    /**
     * @return array<int, array{label: string, type: string, url?: string|null, resource_id?: int|null}>
     */
    private function mainMenuItems(Store $store): array
    {
        $newArrivals = $this->collection($store, 'new-arrivals') ?? $this->collection($store, 'featured');
        $secondaryCollection = $this->collection($store, 't-shirts') ?? $this->collection($store, 'accessories');
        $about = $this->page($store, 'about');

        return array_values(array_filter([
            ['label' => 'Collections', 'type' => 'link', 'url' => '/collections'],
            $newArrivals ? ['label' => $newArrivals->title, 'type' => 'collection', 'resource_id' => $newArrivals->getKey()] : null,
            $secondaryCollection ? ['label' => $secondaryCollection->title, 'type' => 'collection', 'resource_id' => $secondaryCollection->getKey()] : null,
            ['label' => 'Search', 'type' => 'link', 'url' => '/search'],
            $about ? ['label' => 'About', 'type' => 'page', 'resource_id' => $about->getKey()] : null,
        ]));
    }

    /**
     * @return array<int, array{label: string, type: string, url?: string|null, resource_id?: int|null}>
     */
    private function footerMenuItems(Store $store): array
    {
        $sale = $this->collection($store, 'sale');
        $about = $this->page($store, 'about');
        $faq = $this->page($store, 'faq');

        return array_values(array_filter([
            $sale ? ['label' => 'Sale', 'type' => 'collection', 'resource_id' => $sale->getKey()] : null,
            $about ? ['label' => 'About', 'type' => 'page', 'resource_id' => $about->getKey()] : null,
            $faq ? ['label' => 'FAQ', 'type' => 'page', 'resource_id' => $faq->getKey()] : null,
            ['label' => 'Account', 'type' => 'link', 'url' => '/account'],
        ]));
    }

    private function collection(Store $store, string $handle): ?ProductCollection
    {
        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('handle', $handle)
            ->first();
    }

    private function page(Store $store, string $handle): ?Page
    {
        return Page::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('handle', $handle)
            ->first();
    }
}

content:
<?php

namespace Database\Seeders;

use App\Models\Theme;
use App\Models\ThemeSettings;
use App\Services\ThemeSettingsService;
use Illuminate\Database\Seeder;

class ThemeSettingsSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Theme::withoutGlobalScopes()
            ->with('store')
            ->get()
            ->each(function (Theme $theme): void {
                ThemeSettings::withoutGlobalScopes()->updateOrCreate(
                    ['theme_id' => $theme->getKey()],
                    [
                        'settings_json' => app(ThemeSettingsService::class)->defaultsForStore($theme->store),
                        'updated_at' => now(),
                    ],
                );
            });
    }
}

content:
<?php

namespace Database\Seeders;

use App\Models\Page;
use App\Models\Store;
use Illuminate\Database\Seeder;

class PageSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Store::query()->get()->each(function (Store $store): void {
            foreach ($this->pagesFor($store) as $page) {
                Page::withoutGlobalScopes()->updateOrCreate(
                    [
                        'store_id' => $store->getKey(),
                        'handle' => $page['handle'],
                    ],
                    [
                        'title' => $page['title'],
                        'body_html' => $page['body_html'],
                        'status' => 'published',
                        'published_at' => now(),
                    ],
                );
            }
        });
    }

    /**
     * @return array<int, array{handle: string, title: string, body_html: string}>
     */
    private function pagesFor(Store $store): array
    {
        return [
            [
                'handle' => 'about',
                'title' => 'About',
                'body_html' => "
{$store->name} is the demo storefront for this self-contained shop platform.

The catalog includes multi-variant products, sale pricing, inventory rules, backorder handling, and digital product examples for checkout and storefront testing.
",
            ],
            [
                'handle' => 'faq',
                'title' => 'FAQ',
                'body_html' => '
Shipping, payment, returns, and account flows are implemented progressively according to the project roadmap.
',
            ],
            [
                'handle' => 'shipping',
                'title' => 'Shipping',
                'body_html' => '
Shipping zones, rates, and tax-aware checkout calculations are seeded in the checkout phase.
',
            ],
        ];
    }
}

unified_diff:
@@ -22,2 +22,8 @@
             ProductSeeder::class,
+            ThemeSeeder::class,
+            ThemeFileSeeder::class,
+            ThemeSettingsSeeder::class,
+            PageSeeder::class,
+            NavigationMenuSeeder::class,
+            NavigationItemSeeder::class,
             CustomerSeeder::class,

content:
<?php

namespace Database\Seeders;

use App\Models\Theme;
use App\Models\ThemeFile;
use Illuminate\Database\Seeder;

class ThemeFileSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Theme::withoutGlobalScopes()->get()->each(function (Theme $theme): void {
            foreach ([
                'layouts/storefront.blade.php',
                'sections/hero.blade.php',
                'sections/featured-products.blade.php',
            ] as $path) {
                ThemeFile::withoutGlobalScopes()->updateOrCreate(
                    [
                        'theme_id' => $theme->getKey(),
                        'path' => $path,
                    ],
                    [
                        'storage_key' => "themes/{$theme->getKey()}/{$path}",
                        'sha256' => hash('sha256', "{$theme->getKey()}:{$path}"),
                        'byte_size' => 1024,
                    ],
                );
            }
        });
    }
}

content:
<?php

namespace Database\Seeders;

use App\Models\NavigationMenu;
use App\Models\Store;
use Illuminate\Database\Seeder;

class NavigationMenuSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Store::query()->get()->each(function (Store $store): void {
            foreach ([
                'main-menu' => 'Main Menu',
                'footer-menu' => 'Footer Menu',
            ] as $handle => $title) {
                NavigationMenu::withoutGlobalScopes()->updateOrCreate(
                    [
                        'store_id' => $store->getKey(),
                        'handle' => $handle,
                    ],
                    ['title' => $title],
                );
            }
        });
    }
}

content:
<?php

namespace Database\Seeders;

use App\Models\Store;
use App\Models\Theme;
use Illuminate\Database\Seeder;

class ThemeSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Store::query()->orderBy('id')->get()->each(function (Store $store): void {
            Theme::withoutGlobalScopes()->updateOrCreate(
                [
                    'store_id' => $store->getKey(),
                    'name' => "{$store->name} Default",
                ],
                [
                    'version' => '1.0.0',
                    'status' => 'published',
                    'published_at' => now(),
                ],
            );
        });
    }
}


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Providers/AppServiceProvider.php
@@
 namespace App\Providers;

 use App\Auth\CustomerUserProvider;
+use App\Models\Store;
+use App\Services\NavigationService;
+use App\Services\ThemeSettingsService;
 use Carbon\CarbonImmutable;
 use Illuminate\Auth\Middleware\Authenticate;
 use Illuminate\Cache\RateLimiting\Limit;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
@@
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\RateLimiter;
+use Illuminate\Support\Facades\View;
 use Illuminate\Support\ServiceProvider;
 use Illuminate\Validation\Rules\Password;
+use Illuminate\View\View as ViewInstance;
@@
     public function register(): void
     {
         Auth::provider('store_scoped_eloquent', function ($app, array $config): CustomerUserProvider {
             return new CustomerUserProvider($app['hash'], $config['model']);
         });
+
+        $this->app->singleton(ThemeSettingsService::class);
+        $this->app->singleton(NavigationService::class);
     }
@@
     public function boot(): void
     {
         $this->configureDefaults();
+        $this->configureStorefrontViewData();
     }
@@
         });
     }
+
+    protected function configureStorefrontViewData(): void
+    {
+        View::composer('layouts.storefront', function (ViewInstance $view): void {
+            $store = app()->bound('current_store') ? app('current_store') : null;
+
+            if (! $store instanceof Store) {
+                $view->with([
+                    'themeSettings' => [],
+                    'mainNavigation' => [],
+                    'footerNavigation' => [],
+                ]);
+
+                return;
+            }
+
+            $themeSettings = app(ThemeSettingsService::class)->forStore($store);
+            $navigation = app(NavigationService::class);
+
+            $view->with([
+                'themeSettings' => $themeSettings,
+                'mainNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'header.main_menu', 'main-menu')),
+                'footerNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'footer.menu', 'footer-menu')),
+            ]);
+        });
+    }
 }
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Providers/AppServiceProvider.php

unified_diff:
@@ -5,2 +5,5 @@
 use App\Auth\CustomerUserProvider;
+use App\Models\Store;
+use App\Services\NavigationService;
+use App\Services\ThemeSettingsService;
 use Carbon\CarbonImmutable;
@@ -13,4 +16,6 @@
 use Illuminate\Support\Facades\RateLimiter;
+use Illuminate\Support\Facades\View;
 use Illuminate\Support\ServiceProvider;
 use Illuminate\Validation\Rules\Password;
+use Illuminate\View\View as ViewInstance;

@@ -26,2 +31,5 @@
         });
+
+        $this->app->singleton(ThemeSettingsService::class);
+        $this->app->singleton(NavigationService::class);
     }
@@ -34,2 +42,3 @@
         $this->configureDefaults();
+        $this->configureStorefrontViewData();
     }
@@ -77,2 +86,28 @@
     }
+
+    protected function configureStorefrontViewData(): void
+    {
+        View::composer('layouts.storefront', function (ViewInstance $view): void {
+            $store = app()->bound('current_store') ? app('current_store') : null;
+
+            if (! $store instanceof Store) {
+                $view->with([
+                    'themeSettings' => [],
+                    'mainNavigation' => [],
+                    'footerNavigation' => [],
+                ]);
+
+                return;
+            }
+
+            $themeSettings = app(ThemeSettingsService::class)->forStore($store);
+            $navigation = app(NavigationService::class);
+
+            $view->with([
+                'themeSettings' => $themeSettings,
+                'mainNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'header.main_menu', 'main-menu')),
+                'footerNavigation' => $navigation->forHandle($store, data_get($themeSettings, 'footer.menu', 'footer-menu')),
+            ]);
+        });
+    }
 }


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Providers/AppServiceProvider.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Pages/Show.php
@@
 namespace App\Livewire\Storefront\Pages;

-use Illuminate\Support\Str;
+use App\Enums\PageStatus;
+use App\Models\Page;
 use Livewire\Component;

 class Show extends Component
 {
     public string $handle;

     public string $title;

+    public string $bodyHtml;
+
     public function mount(string $handle): void
     {
-        abort_unless(in_array($handle, ['about', 'faq'], true), 404);
+        $page = Page::query()
+            ->where('handle', $handle)
+            ->where('status', PageStatus::Published)
+            ->whereNotNull('published_at')
+            ->firstOrFail();

-        $this->handle = $handle;
-        $this->title = Str::headline($handle);
+        $this->handle = $page->handle;
+        $this->title = $page->title;
+        $this->bodyHtml = (string) $page->body_html;
     }
*** Update File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/pages/show.blade.php
@@
-        @if ($handle === 'about')
-
Acme Fashion is the demo storefront for this self-contained shop platform.

-
The catalog includes multi-variant products, sale pricing, inventory rules, backorder handling, and digital product examples for checkout and storefront testing.

-        @else
-
Frequently asked questions for the demo storefront.

-
Shipping, payment, returns, and account flows are implemented progressively according to the project roadmap.

-        @endif
+

+            {!! $bodyHtml !!}
+

*** Update File: /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Home.php
@@
 use App\Models\Collection;
 use App\Models\Product;
 use App\Models\Store;
+use App\Services\ThemeSettingsService;
 use Illuminate\Support\Collection as SupportCollection;
 use Livewire\Component;
@@
     public function featuredCollections(): SupportCollection
     {
         return Collection::query()
+            ->withCount('products')
             ->where('status', 'active')
             ->orderBy('title')
-            ->limit(4)
+            ->limit((int) data_get($this->themeSettings(), 'home.featured_collection_limit', 4))
             ->get();
     }
+
+    /**
+     * @return array<string, mixed>
+     */
+    public function themeSettings(): array
+    {
+        return app(ThemeSettingsService::class)->forStore($this->store());
+    }
@@
             'store' => $this->store(),
             'featuredProducts' => $this->featuredProducts(),
             'featuredCollections' => $this->featuredCollections(),
+            'themeSettings' => $this->themeSettings(),
         ])->layout('layouts.storefront', [
             'title' => $this->store()->name,
         ]);
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Home.php
@@
             ->with(['variants.inventoryItem'])
             ->withCount('variants')
             ->where('status', 'active')
             ->whereNotNull('published_at')
             ->oldest('id')
-            ->limit(8)
+            ->limit((int) data_get($this->themeSettings(), 'home.featured_product_limit', 8))
             ->get();
     }
*** Update File: /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/home.blade.php
@@

+    @php($hero = data_get($themeSettings, 'home.hero', []))
+

-
New season essentials

-
{{ $store->name }}

-
Browse a scoped demo catalog with variants, inventory states, sale pricing, and digital products.

+
{{ data_get($hero, 'eyebrow') }}

+
{{ data_get($hero, 'heading', $store->name) }}

+
{{ data_get($hero, 'subheading') }}

-                    <flux:button :href="route('collections.show', 'new-arrivals')" wire:navigate variant="primary">Shop new arrivals</flux:button>
-                    <flux:button :href="route('collections.index')" wire:navigate variant="ghost">View collections</flux:button>
+                    <flux:button :href="data_get($hero, 'primary_url', '/collections')" wire:navigate variant="primary">{{ data_get($hero, 'primary_label', 'Shop') }}</flux:button>
+                    <flux:button :href="data_get($hero, 'secondary_url', '/collections')" wire:navigate variant="ghost">{{ data_get($hero, 'secondary_label', 'View collections') }}</flux:button>

@@

                         <flux:icon name="rectangle-stack" class="size-10" />

{{ $collection->title }}

-
{{ $collection->products()->count() }} products

+
{{ $collection->products_count }} products

             @endforeach
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Pages/Show.php
M /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/pages/show.blade.php
M /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Home.php
M /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Home.php
M /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/home.blade.php

unified_diff:
@@ -1,2 +1,4 @@

+    @php($hero = data_get($themeSettings, 'home.hero', []))
+

@@ -5,5 +7,5 @@

-
New season essentials

-
{{ $store->name }}

-
Browse a scoped demo catalog with variants, inventory states, sale pricing, and digital products.

+
{{ data_get($hero, 'eyebrow') }}

+
{{ data_get($hero, 'heading', $store->name) }}

+
{{ data_get($hero, 'subheading') }}

@@ -11,4 +13,4 @@

-                    <flux:button :href="route('collections.show', 'new-arrivals')" wire:navigate variant="primary">Shop new arrivals</flux:button>
-                    <flux:button :href="route('collections.index')" wire:navigate variant="ghost">View collections</flux:button>
+                    <flux:button :href="data_get($hero, 'primary_url', '/collections')" wire:navigate variant="primary">{{ data_get($hero, 'primary_label', 'Shop') }}</flux:button>
+                    <flux:button :href="data_get($hero, 'secondary_url', '/collections')" wire:navigate variant="ghost">{{ data_get($hero, 'secondary_label', 'View collections') }}</flux:button>

@@ -42,3 +44,3 @@

{{ $collection->title }}

-
{{ $collection->products()->count() }} products

+
{{ $collection->products_count }} products

unified_diff:
@@ -6,9 +6,5 @@

-        @if ($handle === 'about')
-
Acme Fashion is the demo storefront for this self-contained shop platform.

-
The catalog includes multi-variant products, sale pricing, inventory rules, backorder handling, and digital product examples for checkout and storefront testing.

-        @else
-
Frequently asked questions for the demo storefront.

-
Shipping, payment, returns, and account flows are implemented progressively according to the project roadmap.

-        @endif
+

+            {!! $bodyHtml !!}
+

unified_diff:
@@ -4,3 +4,4 @@

-use Illuminate\Support\Str;
+use App\Enums\PageStatus;
+use App\Models\Page;
 use Livewire\Component;
@@ -13,8 +14,15 @@

+    public string $bodyHtml;
+
     public function mount(string $handle): void
     {
-        abort_unless(in_array($handle, ['about', 'faq'], true), 404);
+        $page = Page::query()
+            ->where('handle', $handle)
+            ->where('status', PageStatus::Published)
+            ->whereNotNull('published_at')
+            ->firstOrFail();

-        $this->handle = $handle;
-        $this->title = Str::headline($handle);
+        $this->handle = $page->handle;
+        $this->title = $page->title;
+        $this->bodyHtml = (string) $page->body_html;
     }

unified_diff:
@@ -29,3 +29,3 @@
             ->oldest('id')
-            ->limit(8)
+            ->limit((int) data_get($this->themeSettings(), 'home.featured_product_limit', 8))
             ->get();


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Pages/Show.php\nM /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/pages/show.blade.php\nM /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Home.php\nM /Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Home.php\nM /Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/home.blade.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/resources/views/layouts/storefront.blade.php
@@

-        @php($store = app()->bound('current_store') ? app('current_store') : null)
+        @php($store = app()->bound('current_store') ? app('current_store') : null)
+        @php($announcement = data_get($themeSettings ?? [], 'announcement', []))
+        @php($mainLinks = ($mainNavigation ?? []) !== [] ? $mainNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])
+        @php($footerLinks = ($footerNavigation ?? []) !== [] ? $footerNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])

             Skip to main content

-

-            Free shipping on orders over 75.00 EUR
-

+        @if (data_get($announcement, 'enabled', false))
+

+                @if (data_get($announcement, 'url'))
+
+                        {{ data_get($announcement, 'text') }}
+
+                @else
+                    {{ data_get($announcement, 'text') }}
+                @endif
+

+        @endif

-
+         data_get($themeSettings ?? [], 'header.sticky', true),
+        ])>

                     {{ $store?->name ?? config('app.name') }}

-                    Collections
-                    New Arrivals
-                    T-Shirts
-                    Search
-                    About
+                    @foreach ($mainLinks as $item)
+
+                            {{ $item['label'] }}
+
+                    @endforeach

@@

{{ $store?->name ?? config('app.name') }}

-
A self-contained demo storefront with scoped catalog data and checkout-ready products.

+
{{ data_get($themeSettings ?? [], 'footer.tagline') }}

Shop

-                        Collections
-                        Sale
-                        Search
+                        @foreach ($footerLinks as $item)
+
+                                {{ $item['label'] }}
+
+                        @endforeach

*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/resources/views/layouts/storefront.blade.php

unified_diff:
@@ -7,2 +7,5 @@
         @php($store = app()->bound('current_store') ? app('current_store') : null)
+        @php($announcement = data_get($themeSettings ?? [], 'announcement', []))
+        @php($mainLinks = ($mainNavigation ?? []) !== [] ? $mainNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])
+        @php($footerLinks = ($footerNavigation ?? []) !== [] ? $footerNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])

@@ -12,7 +15,18 @@

-

-            Free shipping on orders over 75.00 EUR
-

+        @if (data_get($announcement, 'enabled', false))
+

+                @if (data_get($announcement, 'url'))
+
+                        {{ data_get($announcement, 'text') }}
+
+                @else
+                    {{ data_get($announcement, 'text') }}
+                @endif
+

+        @endif

-
+         data_get($themeSettings ?? [], 'header.sticky', true),
+        ])>

@@ -23,7 +37,7 @@

-                    Collections
-                    New Arrivals
-                    T-Shirts
-                    Search
-                    About
+                    @foreach ($mainLinks as $item)
+
+                            {{ $item['label'] }}
+
+                    @endforeach

@@ -46,3 +60,3 @@

{{ $store?->name ?? config('app.name') }}

-
A self-contained demo storefront with scoped catalog data and checkout-ready products.

+
{{ data_get($themeSettings ?? [], 'footer.tagline') }}

@@ -52,5 +66,7 @@

-                        Collections
-                        Sale
-                        Search
+                        @foreach ($footerLinks as $item)
+
+                                {{ $item['label'] }}
+
+                        @endforeach


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/resources/views/layouts/storefront.blade.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


cmd: sed -n '1,120p' database/migrations/2026_05_03_211419_create_store_settings_table.php


command: sed -n '1,120p' database/migrations/2026_05_03_211419_create_store_settings_table.php

type: read

aggregated_output:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('store_settings', function (Blueprint $table) {
            $table->foreignId('store_id')->primary()->constrained()->cascadeOnDelete();
            $table->text('settings_json')->default('{}');
            $table->timestamp('updated_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('store_settings');
    }
};


output:
Chunk ID: 32837b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 172
Output:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('store_settings', function (Blueprint $table) {
            $table->foreignId('store_id')->primary()->constrained()->cascadeOnDelete();
            $table->text('settings_json')->default('{}');
            $table->timestamp('updated_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('store_settings');
    }
};


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223837_create_theme_settings_table.php
@@
-            $table->json('settings_json')->default('{}');
+            $table->text('settings_json')->default('{}');
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223837_create_theme_settings_table.php

unified_diff:
@@ -15,3 +15,3 @@
             $table->foreignId('theme_id')->primary()->constrained()->cascadeOnDelete();
-            $table->json('settings_json')->default('{}');
+            $table->text('settings_json')->default('{}');
             $table->timestamp('updated_at')->nullable();


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223837_create_theme_settings_table.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


status: completed

input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/tests/Feature/Storefront/ThemeDataTest.php
*** Add File: /Users/fabianwesner/Herd/shop/tests/Feature/Storefront/ThemeDataTest.php
+<?php
+
+use App\Enums\PageStatus;
+use App\Models\NavigationItem;
+use App\Models\NavigationMenu;
+use App\Models\Page;
+use App\Models\Store;
+use App\Models\Theme;
+use App\Models\ThemeFile;
+use App\Models\ThemeSettings;
+use App\Services\NavigationService;
+use App\Services\ThemeSettingsService;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Schema;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function () {
+    Cache::flush();
+});
+
+test('theme page and navigation tables and seeded fixtures exist', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    expect(Schema::hasColumns('themes', ['store_id', 'name', 'version', 'status', 'published_at']))->toBeTrue()
+        ->and(Schema::hasColumns('theme_files', ['theme_id', 'path', 'storage_key', 'sha256', 'byte_size']))->toBeTrue()
+        ->and(Schema::hasColumns('theme_settings', ['theme_id', 'settings_json', 'updated_at']))->toBeTrue()
+        ->and(Schema::hasColumns('pages', ['store_id', 'title', 'handle', 'body_html', 'status', 'published_at']))->toBeTrue()
+        ->and(Schema::hasColumns('navigation_menus', ['store_id', 'handle', 'title']))->toBeTrue()
+        ->and(Schema::hasColumns('navigation_items', ['menu_id', 'type', 'label', 'url', 'resource_id', 'position']))->toBeTrue()
+        ->and(Theme::withoutGlobalScopes()->count())->toBe(2)
+        ->and(ThemeFile::withoutGlobalScopes()->count())->toBe(6)
+        ->and(ThemeSettings::withoutGlobalScopes()->count())->toBe(2)
+        ->and(Page::withoutGlobalScopes()->count())->toBe(6)
+        ->and(NavigationMenu::withoutGlobalScopes()->count())->toBe(4)
+        ->and(NavigationItem::withoutGlobalScopes()->count())->toBeGreaterThanOrEqual(10);
+});
+
+test('theme page and navigation models are scoped to the resolved store', function () {
+    $firstStore = Store::factory()->create();
+    $secondStore = Store::factory()->create();
+
+    Theme::factory()->published()->create(['store_id' => $firstStore->getKey(), 'name' => 'First Theme']);
+    Theme::factory()->published()->create(['store_id' => $secondStore->getKey(), 'name' => 'Second Theme']);
+    Page::factory()->published()->create(['store_id' => $firstStore->getKey(), 'handle' => 'first-page']);
+    Page::factory()->published()->create(['store_id' => $secondStore->getKey(), 'handle' => 'second-page']);
+    NavigationMenu::factory()->create(['store_id' => $firstStore->getKey(), 'handle' => 'first-menu']);
+    NavigationMenu::factory()->create(['store_id' => $secondStore->getKey(), 'handle' => 'second-menu']);
+
+    app()->instance('current_store', $firstStore);
+
+    expect(Theme::query()->pluck('name')->all())->toBe(['First Theme'])
+        ->and(Page::query()->pluck('handle')->all())->toBe(['first-page'])
+        ->and(NavigationMenu::query()->pluck('handle')->all())->toBe(['first-menu']);
+});
+
+test('theme and navigation child models are scoped through their parent store', function () {
+    $firstStore = Store::factory()->create();
+    $secondStore = Store::factory()->create();
+    $firstTheme = Theme::factory()->published()->create(['store_id' => $firstStore->getKey()]);
+    $secondTheme = Theme::factory()->published()->create(['store_id' => $secondStore->getKey()]);
+    $firstMenu = NavigationMenu::factory()->create(['store_id' => $firstStore->getKey()]);
+    $secondMenu = NavigationMenu::factory()->create(['store_id' => $secondStore->getKey()]);
+
+    ThemeFile::factory()->create(['theme_id' => $firstTheme->getKey(), 'path' => 'first.blade.php']);
+    ThemeFile::factory()->create(['theme_id' => $secondTheme->getKey(), 'path' => 'second.blade.php']);
+    ThemeSettings::factory()->create(['theme_id' => $firstTheme->getKey()]);
+    ThemeSettings::factory()->create(['theme_id' => $secondTheme->getKey()]);
+    NavigationItem::factory()->create(['menu_id' => $firstMenu->getKey(), 'label' => 'First']);
+    NavigationItem::factory()->create(['menu_id' => $secondMenu->getKey(), 'label' => 'Second']);
+
+    app()->instance('current_store', $firstStore);
+
+    expect(ThemeFile::query()->pluck('path')->all())->toBe(['first.blade.php'])
+        ->and(ThemeSettings::query()->count())->toBe(1)
+        ->and(NavigationItem::query()->pluck('label')->all())->toBe(['First']);
+});
+
+test('theme settings service loads cached published settings with defaults', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+    $settings = app(ThemeSettingsService::class)->forStore($store);
+
+    expect($settings['announcement']['text'])->toBe('Free shipping on orders over 75.00 EUR')
+        ->and($settings['home']['hero']['heading'])->toBe('Acme Fashion')
+        ->and(Cache::has("theme_settings:{$store->getKey()}"))->toBeTrue();
+});
+
+test('navigation service resolves resource URLs for seeded menus', function () {
+    $this->seed(DatabaseSeeder::class);
+
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+    $menu = NavigationMenu::withoutGlobalScopes()
+        ->where('store_id', $store->getKey())
+        ->where('handle', 'main-menu')
+        ->firstOrFail();
+
+    $items = app(NavigationService::class)->buildTree($menu);
+
+    expect(collect($items)->pluck('label')->all())->toContain('New Arrivals', 'T-Shirts', 'About')
+        ->and(collect($items)->firstWhere('label', 'New Arrivals')['url'])->toBe('/collections/new-arrivals')
+        ->and(collect($items)->firstWhere('label', 'About')['url'])->toBe('/pages/about');
+});
+
+test('storefront pages render published database content only', function () {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+
+    Page::withoutGlobalScopes()->create([
+        'store_id' => $store->getKey(),
+        'title' => 'Private Draft',
+        'handle' => 'private-draft',
+        'body_html' => '
This should not render.
',
+        'status' => PageStatus::Draft,
+        'published_at' => null,
+    ]);
+
+    $this->withHeader('Host', 'shop.test')
+        ->get('/pages/about')
+        ->assertSuccessful()
+        ->assertSee('About')
+        ->assertSee('Acme Fashion is the demo storefront');
+
+    $this->withHeader('Host', 'shop.test')
+        ->get('/pages/private-draft')
+        ->assertNotFound();
+});
*** End Patch