diff --git a/Modules/Content/Config/config.php b/Modules/Content/Config/config.php new file mode 100644 index 0000000..b0ba6d5 --- /dev/null +++ b/Modules/Content/Config/config.php @@ -0,0 +1,5 @@ + 'home', + 'regions' => ['header', 'body', 'footer'], +]; diff --git a/Modules/Content/Console/InstallSaaslandContent.php b/Modules/Content/Console/InstallSaaslandContent.php new file mode 100644 index 0000000..371729d --- /dev/null +++ b/Modules/Content/Console/InstallSaaslandContent.php @@ -0,0 +1,50 @@ +option('force')) PageContent::query()->where('page_key','home')->delete(); + $definitions = $this->definitions(); + $order = ['site-header'=>['header',10], 'hero'=>['body',10], 'feature-grid'=>['body',20], 'product-feed'=>['body',30], 'testimonial-feed'=>['body',40], 'team-feed'=>['body',50], 'blog-feed'=>['body',60], 'news-feed'=>['body',70], 'faq-feed'=>['body',80], 'newsletter-form'=>['body',90], 'call-to-action'=>['body',100], 'site-footer'=>['footer',10]]; + foreach ($definitions as $key => $definition) { + $type = ContentType::query()->updateOrCreate(['key'=>$key], $definition['type']); + $document = ContentDocument::query()->updateOrCreate(['slug'=>'saasland-'.$key], ['content_type_id'=>$type->id,'title'=>$definition['title'],'data'=>$definition['data'],'status'=>'published','published_at'=>now()]); + [$region,$sort] = $order[$key]; + PageContent::query()->updateOrCreate(['page_key'=>'home','region'=>$region,'sort_order'=>$sort], ['content_document_id'=>$document->id,'is_enabled'=>true]); + } + }); + $this->components->info('Saasland content composition installed.'); + return self::SUCCESS; + } + + private function definitions(): array + { + $type = fn(string $name,string $renderer,array $regions) => ['name'=>$name,'renderer'=>$renderer,'allowed_regions'=>$regions,'is_system'=>true,'is_active'=>true,'schema'=>[]]; + return [ + 'site-header'=>['title'=>'Main website header','type'=>$type('Site header','site-header',['header']),'data'=>['show_login'=>true,'show_registration'=>true,'sticky'=>true]], + 'hero'=>['title'=>'Homepage hero','type'=>$type('Hero','hero',['body']),'data'=>['eyebrow'=>'A better way to run your business','heading'=>'Transform Your Business With Our SaaS Solution','description'=>'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.','primary_label'=>'Get started free','primary_url'=>'/register','secondary_label'=>'Book demo','secondary_url'=>'#contact','show_metrics'=>true]], + 'feature-grid'=>['title'=>'Core features','type'=>$type('Feature grid','feature-grid',['body']),'data'=>['eyebrow'=>'Powerful features','heading'=>'Everything your team needs to grow','description'=>'Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.','items'=>[['title'=>'Smart automation','description'=>'Remove repetitive work and delays.'],['title'=>'Real-time analytics','description'=>'Turn activity into useful business insight.'],['title'=>'Content operations','description'=>'Create, review and publish confidently.'],['title'=>'Secure by design','description'=>'Protect users and data with dependable controls.'],['title'=>'Flexible integrations','description'=>'Connect the tools your organisation already uses.'],['title'=>'Modular architecture','description'=>'Activate only the capabilities each site needs.']]]], + 'product-feed'=>['title'=>'Pricing plans','type'=>$type('Product feed','product-feed',['body']),'data'=>['eyebrow'=>'Simple pricing','heading'=>'Choose a plan that fits','description'=>'Plans are managed from Products & Pricing in the CMS.','limit'=>6]], + 'testimonial-feed'=>['title'=>'Customer stories','type'=>$type('Testimonial feed','testimonial-feed',['body']),'data'=>['eyebrow'=>'Customer stories','heading'=>'Trusted by ambitious teams','limit'=>6]], + 'team-feed'=>['title'=>'Team','type'=>$type('Team feed','team-feed',['body']),'data'=>['eyebrow'=>'Our team','heading'=>'People behind the platform','limit'=>8]], + 'blog-feed'=>['title'=>'Latest blog posts','type'=>$type('Blog feed','blog-feed',['body']),'data'=>['eyebrow'=>'Insights','heading'=>'Latest from the blog','limit'=>3]], + 'news-feed'=>['title'=>'Latest news','type'=>$type('News feed','news-feed',['body']),'data'=>['eyebrow'=>'Newsroom','heading'=>'Latest news','limit'=>3]], + 'faq-feed'=>['title'=>'Frequently asked questions','type'=>$type('FAQ feed','faq-feed',['body']),'data'=>['eyebrow'=>'FAQ','heading'=>'Answers before you begin','limit'=>12]], + 'newsletter-form'=>['title'=>'Newsletter signup','type'=>$type('Newsletter form','newsletter-form',['body']),'data'=>['eyebrow'=>'Newsletter','heading'=>'Useful updates, no noise','description'=>'Receive product news, publishing guidance, and practical platform updates.','button_label'=>'Subscribe']], + 'call-to-action'=>['title'=>'Homepage final call to action','type'=>$type('Call to action','call-to-action',['body']),'data'=>['heading'=>'Ready to transform your business?','description'=>'Start building a faster, more connected operation today.','button_label'=>'Get started free','button_url'=>'/register']], + 'site-footer'=>['title'=>'Main website footer','type'=>$type('Site footer','site-footer',['footer']),'data'=>['description'=>'One connected platform for modern teams.','show_social_links'=>true,'show_back_to_top'=>true]], + ]; + } +} diff --git a/Modules/Content/Database/Migrations/2026_07_27_000000_create_content_composition_tables.php b/Modules/Content/Database/Migrations/2026_07_27_000000_create_content_composition_tables.php new file mode 100644 index 0000000..2fcd905 --- /dev/null +++ b/Modules/Content/Database/Migrations/2026_07_27_000000_create_content_composition_tables.php @@ -0,0 +1,62 @@ +id(); + $table->string('name'); + $table->string('key')->unique(); + $table->text('description')->nullable(); + $table->json('schema')->nullable(); + $table->string('renderer'); + $table->json('allowed_regions')->nullable(); + $table->boolean('is_system')->default(false); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + + Schema::create('content_documents', function (Blueprint $table): void { + $table->id(); + $table->foreignId('content_type_id')->constrained('content_types')->cascadeOnDelete(); + $table->string('title'); + $table->string('slug')->unique(); + $table->json('data')->nullable(); + $table->string('status')->default('draft')->index(); + $table->timestamp('published_at')->nullable()->index(); + $table->timestamp('expires_at')->nullable(); + $table->string('language', 12)->nullable(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('page_content', function (Blueprint $table): void { + $table->id(); + $table->foreignId('page_id')->nullable()->constrained('pages')->cascadeOnDelete(); + $table->string('page_key')->nullable()->index(); + $table->foreignId('content_document_id')->constrained('content_documents')->cascadeOnDelete(); + $table->string('region')->index(); + $table->unsignedInteger('sort_order')->default(0); + $table->boolean('is_enabled')->default(true); + $table->json('visibility_rules')->nullable(); + $table->json('presentation')->nullable(); + $table->timestamp('starts_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->timestamps(); + $table->unique(['page_key', 'region', 'sort_order'], 'page_content_key_region_order_unique'); + }); + } + + public function down(): void + { + Schema::dropIfExists('page_content'); + Schema::dropIfExists('content_documents'); + Schema::dropIfExists('content_types'); + } +}; diff --git a/Modules/Content/Http/Controllers/ContentDocumentController.php b/Modules/Content/Http/Controllers/ContentDocumentController.php new file mode 100644 index 0000000..587c03f --- /dev/null +++ b/Modules/Content/Http/Controllers/ContentDocumentController.php @@ -0,0 +1,20 @@ +ContentDocument::query()->with('type')->withCount('placements')->orderBy('title')->paginate(30)]); } + public function edit(ContentDocument $document): View { $document->load('type','placements'); return view('content::admin.edit',['document'=>$document]); } + public function update(Request $request, ContentDocument $document): RedirectResponse + { + $validated=$request->validate(['title'=>['required','string','max:255'],'status'=>['required','in:draft,published'],'data_json'=>['required','json']]); + $document->update(['title'=>$validated['title'],'status'=>$validated['status'],'published_at'=>$validated['status']==='published'?($document->published_at??now()):null,'data'=>json_decode($validated['data_json'],true,512,JSON_THROW_ON_ERROR),'updated_by'=>$request->user()?->id]); + return back()->with('status','Content document updated.'); + } +} diff --git a/Modules/Content/Http/Controllers/PageCompositionController.php b/Modules/Content/Http/Controllers/PageCompositionController.php new file mode 100644 index 0000000..2b6a0de --- /dev/null +++ b/Modules/Content/Http/Controllers/PageCompositionController.php @@ -0,0 +1,19 @@ +$pageKey,'placements'=>PageContent::query()->with('document.type')->where('page_key',$pageKey)->orderByRaw("CASE region WHEN 'header' THEN 1 WHEN 'body' THEN 2 ELSE 3 END")->orderBy('sort_order')->get()->groupBy('region')]); } + public function update(Request $request, PageContent $placement): RedirectResponse + { + $validated=$request->validate(['sort_order'=>['required','integer','min:0'],'is_enabled'=>['nullable','boolean']]); + $placement->update(['sort_order'=>$validated['sort_order'],'is_enabled'=>$request->boolean('is_enabled')]); + return back()->with('status','Page placement updated.'); + } +} diff --git a/Modules/Content/Models/ContentDocument.php b/Modules/Content/Models/ContentDocument.php new file mode 100644 index 0000000..edf93be --- /dev/null +++ b/Modules/Content/Models/ContentDocument.php @@ -0,0 +1,17 @@ +'array','published_at'=>'datetime','expires_at'=>'datetime']; } + public function type(): BelongsTo { return $this->belongsTo(ContentType::class, 'content_type_id'); } + public function placements(): HasMany { return $this->hasMany(PageContent::class); } + public function scopePublished($query) { return $query->where('status','published')->where(fn($q) => $q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at','>',now())); } +} diff --git a/Modules/Content/Models/ContentType.php b/Modules/Content/Models/ContentType.php new file mode 100644 index 0000000..6b0ffd7 --- /dev/null +++ b/Modules/Content/Models/ContentType.php @@ -0,0 +1,12 @@ +'array','allowed_regions'=>'array','is_system'=>'boolean','is_active'=>'boolean']; } + public function documents(): HasMany { return $this->hasMany(ContentDocument::class); } +} diff --git a/Modules/Content/Models/PageContent.php b/Modules/Content/Models/PageContent.php new file mode 100644 index 0000000..cb204f3 --- /dev/null +++ b/Modules/Content/Models/PageContent.php @@ -0,0 +1,15 @@ +'boolean','visibility_rules'=>'array','presentation'=>'array','starts_at'=>'datetime','ends_at'=>'datetime']; } + public function document(): BelongsTo { return $this->belongsTo(ContentDocument::class, 'content_document_id'); } + public function page(): BelongsTo { return $this->belongsTo(Page::class); } +} diff --git a/Modules/Content/Providers/ContentServiceProvider.php b/Modules/Content/Providers/ContentServiceProvider.php new file mode 100644 index 0000000..0ba0b27 --- /dev/null +++ b/Modules/Content/Providers/ContentServiceProvider.php @@ -0,0 +1,22 @@ +mergeConfigFrom(module_path('Content','Config/config.php'),'content'); + $this->app->singleton(PageRenderer::class); + } + public function boot(): void + { + $this->loadMigrationsFrom(module_path('Content','Database/Migrations')); + $this->loadViewsFrom(module_path('Content','Resources/views'),'content'); + $this->loadRoutesFrom(module_path('Content','Routes/web.php')); + if ($this->app->runningInConsole()) $this->commands([InstallSaaslandContent::class]); + } +} diff --git a/Modules/Content/README.md b/Modules/Content/README.md new file mode 100644 index 0000000..d2e7942 --- /dev/null +++ b/Modules/Content/README.md @@ -0,0 +1,33 @@ +# Content composition module + +This module separates public pages into reusable typed content documents placed in named regions. + +## Install the baseline composition + +```bash +php artisan migrate +php artisan content:install-saasland +``` + +Use `--force` to replace the existing `home` placements while retaining reusable documents. + +```bash +php artisan content:install-saasland --force +``` + +## Administration + +After signing in, open: + +- `/admin/content` for reusable content documents. +- `/admin/content/pages/home` for the homepage header/body/footer composition. + +Documents contain structured JSON data and can be shared by several page placements. Placements own ordering, enabled state, scheduling, visibility and presentation metadata. The Saasland frontend falls back to its original hard-coded homepage until the migration and installer command have been run. + +## Theme renderers + +Saasland renderers are stored under: + +`resources/themes/saasland/views/documents` + +The renderer name is defined by each `content_types.renderer` value. Missing renderers fail safely through `content::documents.unsupported`. diff --git a/Modules/Content/Resources/views/admin/composition.blade.php b/Modules/Content/Resources/views/admin/composition.blade.php new file mode 100644 index 0000000..67c433b --- /dev/null +++ b/Modules/Content/Resources/views/admin/composition.blade.php @@ -0,0 +1 @@ +

Page composition: {{ $pageKey }}

@if(session('status'))

{{ session('status') }}

@endif @foreach(['header','body','footer'] as $region)

{{ $region }}

@forelse($placements->get($region,collect()) as $placement)
@csrf @method('PUT')
{{ $placement->document?->title }}
{{ $placement->document?->type?->name }}
Edit
@empty

No documents assigned.

@endforelse
@endforeach
diff --git a/Modules/Content/Resources/views/admin/edit.blade.php b/Modules/Content/Resources/views/admin/edit.blade.php new file mode 100644 index 0000000..e7ec84d --- /dev/null +++ b/Modules/Content/Resources/views/admin/edit.blade.php @@ -0,0 +1 @@ +
@csrf @method('PUT')

{{ $document->title }}

@if(session('status'))

{{ session('status') }}

@endif@error('data_json')

{{ $message }}

@enderror
diff --git a/Modules/Content/Resources/views/admin/index.blade.php b/Modules/Content/Resources/views/admin/index.blade.php new file mode 100644 index 0000000..e19be1e --- /dev/null +++ b/Modules/Content/Resources/views/admin/index.blade.php @@ -0,0 +1,5 @@ + +

Content documents

Reusable typed documents used by page regions.

+
Edit homepage composition
+
@foreach($documents as $document)@endforeach
DocumentTypeStatusUses
{{ $document->title }}{{ $document->type?->name }}{{ ucfirst($document->status) }}{{ $document->placements_count }}Edit
{{ $documents->links() }}
+
diff --git a/Modules/Content/Resources/views/documents/unsupported.blade.php b/Modules/Content/Resources/views/documents/unsupported.blade.php new file mode 100644 index 0000000..b80545d --- /dev/null +++ b/Modules/Content/Resources/views/documents/unsupported.blade.php @@ -0,0 +1,3 @@ +@if(app()->hasDebugModeEnabled()) + +@endif diff --git a/Modules/Content/Routes/web.php b/Modules/Content/Routes/web.php new file mode 100644 index 0000000..6f0a304 --- /dev/null +++ b/Modules/Content/Routes/web.php @@ -0,0 +1,12 @@ +prefix('admin/content')->name('content.')->group(function (): void { + Route::get('/', [ContentDocumentController::class,'index'])->name('index'); + Route::get('/documents/{document}/edit', [ContentDocumentController::class,'edit'])->name('documents.edit'); + Route::put('/documents/{document}', [ContentDocumentController::class,'update'])->name('documents.update'); + Route::get('/pages/{pageKey}', [PageCompositionController::class,'edit'])->name('pages.edit'); + Route::put('/placements/{placement}', [PageCompositionController::class,'update'])->name('placements.update'); +}); diff --git a/Modules/Content/Services/PageRenderer.php b/Modules/Content/Services/PageRenderer.php new file mode 100644 index 0000000..a13c588 --- /dev/null +++ b/Modules/Content/Services/PageRenderer.php @@ -0,0 +1,38 @@ +where('page_key',$pageKey)->where('is_enabled',true)->exists(); } + catch (Throwable) { return false; } + } + + public function renderRegion(string $pageKey, string $region): string + { + if (! $this->hasComposition($pageKey)) return ''; + + $placements = PageContent::query() + ->with('document.type') + ->where('page_key', $pageKey) + ->where('region', $region) + ->where('is_enabled', true) + ->where(fn($q) => $q->whereNull('starts_at')->orWhere('starts_at','<=',now())) + ->where(fn($q) => $q->whereNull('ends_at')->orWhere('ends_at','>',now())) + ->orderBy('sort_order')->get(); + + return $placements->map(function (PageContent $placement): string { + $document = $placement->document; + if (! $document || ! $document->type || $document->status !== 'published') return ''; + $view = 'stylist-theme-saasland::documents.'.$document->type->renderer; + if (! View::exists($view)) $view = 'content::documents.unsupported'; + return View::make($view, ['document'=>$document,'data'=>$document->data ?? [],'placement'=>$placement])->render(); + })->implode("\n"); + } +} diff --git a/Modules/Content/module.json b/Modules/Content/module.json new file mode 100644 index 0000000..e34671c --- /dev/null +++ b/Modules/Content/module.json @@ -0,0 +1,12 @@ +{ + "name": "Content", + "alias": "content", + "version": "1.0.0", + "description": "Composable typed content documents and page regions.", + "keywords": ["cms", "content", "regions", "composition"], + "priority": 22, + "providers": ["Modules\\Content\\Providers\\ContentServiceProvider"], + "aliases": {}, + "files": [], + "requires": ["Core", "Pages", "Stylist"] +} diff --git a/Modules/Stylist/Services/FrontendContent.php b/Modules/Stylist/Services/FrontendContent.php index f4e5875..e57aac5 100644 --- a/Modules/Stylist/Services/FrontendContent.php +++ b/Modules/Stylist/Services/FrontendContent.php @@ -17,13 +17,16 @@ final class FrontendContent /** @return array */ public function homepage(array $settings): array { + // Always provide source collections. Visibility is owned by page placements + // in the composable content system; the legacy theme still applies its + // existing show_* settings while no composition has been installed. return [ - 'pricingPlans' => $this->enabled($settings, 'show_pricing', true) ? $this->pricingPlans() : collect(), - 'testimonials' => $this->enabled($settings, 'show_testimonials', true) ? $this->testimonials() : collect(), - 'faqs' => $this->enabled($settings, 'show_faq', true) ? $this->faqs() : collect(), - 'teamMembers' => $this->enabled($settings, 'show_team', false) ? $this->team() : collect(), - 'latestBlogPosts' => $this->enabled($settings, 'show_blog', true) ? $this->blogPosts() : collect(), - 'latestNewsArticles' => $this->enabled($settings, 'show_news', true) ? $this->newsArticles() : collect(), + 'pricingPlans' => $this->pricingPlans(), + 'testimonials' => $this->testimonials(), + 'faqs' => $this->faqs(), + 'teamMembers' => $this->team(), + 'latestBlogPosts' => $this->blogPosts(), + 'latestNewsArticles' => $this->newsArticles(), ]; } diff --git a/modules_statuses.json b/modules_statuses.json index f367ed8..30a8fa7 100644 --- a/modules_statuses.json +++ b/modules_statuses.json @@ -45,5 +45,6 @@ "Analytics": true, "Builder": true, "Marketplace": true, - "CloudDevOps": true + "CloudDevOps": true, + "Content": true } diff --git a/resources/themes/saasland/views/documents/blog-feed.blade.php b/resources/themes/saasland/views/documents/blog-feed.blade.php new file mode 100644 index 0000000..4a06c8a --- /dev/null +++ b/resources/themes/saasland/views/documents/blog-feed.blade.php @@ -0,0 +1 @@ +@if($latestBlogPosts->isNotEmpty())
{{ $data['eyebrow'] ?? '' }}

{{ $data['heading'] ?? '' }}

@foreach($latestBlogPosts->take($data['limit'] ?? 3) as $post)
{{ optional($post->published_at)->format('d M Y') }}

{{ $post->title }}

{{ $post->excerpt }}

Read article →
@endforeach
@endif diff --git a/resources/themes/saasland/views/documents/call-to-action.blade.php b/resources/themes/saasland/views/documents/call-to-action.blade.php new file mode 100644 index 0000000..1c8b7a4 --- /dev/null +++ b/resources/themes/saasland/views/documents/call-to-action.blade.php @@ -0,0 +1 @@ +

{{ $data['heading'] ?? '' }}

{{ $data['description'] ?? '' }}

{{ $data['button_label'] ?? 'Get started' }} →
diff --git a/resources/themes/saasland/views/documents/faq-feed.blade.php b/resources/themes/saasland/views/documents/faq-feed.blade.php new file mode 100644 index 0000000..d55e40f --- /dev/null +++ b/resources/themes/saasland/views/documents/faq-feed.blade.php @@ -0,0 +1 @@ +@if($faqs->isNotEmpty())

{{ $data['heading'] ?? '' }}

@foreach($faqs->take($data['limit'] ?? 12) as $faq)

{{ $faq->answer }}

@endforeach
@endif diff --git a/resources/themes/saasland/views/documents/feature-grid.blade.php b/resources/themes/saasland/views/documents/feature-grid.blade.php new file mode 100644 index 0000000..e4c4755 --- /dev/null +++ b/resources/themes/saasland/views/documents/feature-grid.blade.php @@ -0,0 +1 @@ +
{{ $data['eyebrow'] ?? '' }}

{{ $data['heading'] ?? '' }}

{{ $data['description'] ?? '' }}

@foreach($data['items'] ?? [] as $i => $item)
{{ str_pad((string)($i+1),2,'0',STR_PAD_LEFT) }}

{{ $item['title'] ?? '' }}

{{ $item['description'] ?? '' }}

@endforeach
diff --git a/resources/themes/saasland/views/documents/hero.blade.php b/resources/themes/saasland/views/documents/hero.blade.php new file mode 100644 index 0000000..db97e84 --- /dev/null +++ b/resources/themes/saasland/views/documents/hero.blade.php @@ -0,0 +1,5 @@ +
+{{ $data['eyebrow'] ?? '' }}

{{ $data['heading'] ?? $document->title }}

{{ $data['description'] ?? '' }}

+ +@if($data['show_metrics'] ?? false)
Performance overviewLive
Monthly revenue£48,240+18.4%
Active users12,842+9.2%
Conversion7.8%+2.1%
@endif +
diff --git a/resources/themes/saasland/views/documents/news-feed.blade.php b/resources/themes/saasland/views/documents/news-feed.blade.php new file mode 100644 index 0000000..228592d --- /dev/null +++ b/resources/themes/saasland/views/documents/news-feed.blade.php @@ -0,0 +1 @@ +@if($latestNewsArticles->isNotEmpty())
{{ $data['eyebrow'] ?? '' }}

{{ $data['heading'] ?? '' }}

@foreach($latestNewsArticles->take($data['limit'] ?? 3) as $article)
{{ optional($article->published_at)->format('d M Y') }}

{{ $article->title }}

{{ $article->excerpt }}

Read update →
@endforeach
@endif diff --git a/resources/themes/saasland/views/documents/newsletter-form.blade.php b/resources/themes/saasland/views/documents/newsletter-form.blade.php new file mode 100644 index 0000000..fddb426 --- /dev/null +++ b/resources/themes/saasland/views/documents/newsletter-form.blade.php @@ -0,0 +1 @@ +
diff --git a/resources/themes/saasland/views/documents/product-feed.blade.php b/resources/themes/saasland/views/documents/product-feed.blade.php new file mode 100644 index 0000000..40be1dd --- /dev/null +++ b/resources/themes/saasland/views/documents/product-feed.blade.php @@ -0,0 +1 @@ +@if($pricingPlans->isNotEmpty())
{{ $data['eyebrow'] ?? '' }}

{{ $data['heading'] ?? '' }}

{{ $data['description'] ?? '' }}

@foreach($pricingPlans->take($data['limit'] ?? 6) as $plan)@endforeach
@endif diff --git a/resources/themes/saasland/views/documents/site-footer.blade.php b/resources/themes/saasland/views/documents/site-footer.blade.php new file mode 100644 index 0000000..9a69066 --- /dev/null +++ b/resources/themes/saasland/views/documents/site-footer.blade.php @@ -0,0 +1 @@ +@include('stylist-theme-saasland::partials.footer') diff --git a/resources/themes/saasland/views/documents/site-header.blade.php b/resources/themes/saasland/views/documents/site-header.blade.php new file mode 100644 index 0000000..0812161 --- /dev/null +++ b/resources/themes/saasland/views/documents/site-header.blade.php @@ -0,0 +1 @@ +@include('stylist-theme-saasland::partials.header') diff --git a/resources/themes/saasland/views/documents/team-feed.blade.php b/resources/themes/saasland/views/documents/team-feed.blade.php new file mode 100644 index 0000000..075a810 --- /dev/null +++ b/resources/themes/saasland/views/documents/team-feed.blade.php @@ -0,0 +1 @@ +@if($teamMembers->isNotEmpty())
{{ $data['eyebrow'] ?? '' }}

{{ $data['heading'] ?? '' }}

@foreach($teamMembers->take($data['limit'] ?? 8) as $member)

{{ $member->name }}

{{ $member->job_title }}

{{ $member->biography }}

@endforeach
@endif diff --git a/resources/themes/saasland/views/documents/testimonial-feed.blade.php b/resources/themes/saasland/views/documents/testimonial-feed.blade.php new file mode 100644 index 0000000..2d79292 --- /dev/null +++ b/resources/themes/saasland/views/documents/testimonial-feed.blade.php @@ -0,0 +1 @@ +@if($testimonials->isNotEmpty())
{{ $data['eyebrow'] ?? '' }}

{{ $data['heading'] ?? '' }}

@foreach($testimonials->take($data['limit'] ?? 6) as $testimonial)
“{{ $testimonial->content }}”
{{ $testimonial->name }}@if($testimonial->company) · {{ $testimonial->company }}@endif
@endforeach
@endif diff --git a/resources/themes/saasland/views/home.blade.php b/resources/themes/saasland/views/home.blade.php index 405a28b..f08bd71 100644 --- a/resources/themes/saasland/views/home.blade.php +++ b/resources/themes/saasland/views/home.blade.php @@ -4,71 +4,15 @@ $title = $siteTitle; $description = $siteDescription; $hero = true; - $enabled = fn (string $key, bool $default = true) => filter_var($themeSettings[$key] ?? $default, FILTER_VALIDATE_BOOL); - $heroTitle = filled($themeSettings['hero_title'] ?? null) ? $themeSettings['hero_title'] : 'Transform Your Business With Our SaaS Solution'; - $heroText = filled($themeSettings['hero_description'] ?? null) ? $themeSettings['hero_description'] : 'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.'; + $pageKey = 'home'; + $contentRenderer = app(\Modules\Content\Services\PageRenderer::class); + $hasComposition = $contentRenderer->hasComposition($pageKey); @endphp @section('content') -
-
-
- {{ $themeSettings['hero_eyebrow'] ?? 'A better way to run your business' }} -

{{ $heroTitle }}

-

{{ $heroText }}

- - @if($enabled('show_metrics', true)) -
-
Performance overviewLive
-
Monthly revenue£48,240+18.4%
Active users12,842+9.2%
Conversion7.8%+2.1%
-
-
- @endif -
-
- -@if($enabled('show_features', true)) -
Powerful features

Everything your team needs to grow

Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.

-@foreach([['Smart automation','Remove repetitive work and delays.'],['Real-time analytics','Turn activity into useful business insight.'],['Content operations','Create, review and publish confidently.'],['Secure by design','Protect users and data with dependable controls.'],['Flexible integrations','Connect the tools your organisation already uses.'],['Modular architecture','Activate only the capabilities each site needs.']] as $i => $feature) -
{{ str_pad((string)($i+1),2,'0',STR_PAD_LEFT) }}

{{ $feature[0] }}

{{ $feature[1] }}

-@endforeach -
-@endif - -@if($enabled('show_pricing', true) && $pricingPlans->isNotEmpty()) -
Simple pricing

Choose a plan that fits

Plans are managed from Products & Pricing in the CMS.

-@foreach($pricingPlans as $plan) - -@endforeach -
-@endif - -@if($enabled('show_testimonials', true) && $testimonials->isNotEmpty()) -
Customer stories

Trusted by ambitious teams

@foreach($testimonials as $testimonial)
“{{ $testimonial->content }}”
{{ $testimonial->name }}@if($testimonial->company) · {{ $testimonial->company }}@endif
@endforeach
-@endif - -@if($enabled('show_team', false) && $teamMembers->isNotEmpty()) -
Our team

People behind the platform

@foreach($teamMembers as $member)

{{ $member->name }}

{{ $member->job_title }}

{{ $member->biography }}

@endforeach
-@endif - -@if($enabled('show_blog', true) && $latestBlogPosts->isNotEmpty()) -
Insights

Latest from the blog

@foreach($latestBlogPosts as $post)
{{ optional($post->published_at)->format('d M Y') }}

{{ $post->title }}

{{ $post->excerpt }}

Read article →
@endforeach
-@endif - -@if($enabled('show_news', true) && $latestNewsArticles->isNotEmpty()) -
Newsroom

Latest news

@foreach($latestNewsArticles as $article)
{{ optional($article->published_at)->format('d M Y') }}

{{ $article->title }}

{{ $article->excerpt }}

Read update →
@endforeach
-@endif - -@if($enabled('show_faq', true) && $faqs->isNotEmpty()) -

Answers before you begin

@foreach($faqs as $faq)

{{ $faq->answer }}

@endforeach
-@endif - -@if($enabled('show_newsletter', true)) -
-@endif - -

{{ $themeSettings['cta_title'] ?? 'Ready to transform your business?' }}

{{ $themeSettings['cta_description'] ?? 'Start building a faster, more connected operation today.' }}

@authOpen dashboard →@else @if(Route::has('register'))Get started free →@endif @endauth
+ @if($hasComposition) + {!! $contentRenderer->renderRegion($pageKey, 'body') !!} + @else + @include('stylist-theme-saasland::legacy-home-content') + @endif @endsection diff --git a/resources/themes/saasland/views/layouts/app.blade.php b/resources/themes/saasland/views/layouts/app.blade.php index 9503a37..ffeecc5 100644 --- a/resources/themes/saasland/views/layouts/app.blade.php +++ b/resources/themes/saasland/views/layouts/app.blade.php @@ -25,7 +25,16 @@
- @include('stylist-theme-saasland::partials.header') + @php + $compositionRenderer = app(\Modules\Content\Services\PageRenderer::class); + $compositionPageKey = $pageKey ?? null; + $hasPageComposition = $compositionPageKey && $compositionRenderer->hasComposition($compositionPageKey); + @endphp + @if($hasPageComposition) + {!! $compositionRenderer->renderRegion($compositionPageKey, 'header') !!} + @else + @include('stylist-theme-saasland::partials.header') + @endif @unless($hero)
@@ -52,7 +61,11 @@ @yield('content') - @include('stylist-theme-saasland::partials.footer') + @if($hasPageComposition) + {!! $compositionRenderer->renderRegion($compositionPageKey, 'footer') !!} + @else + @include('stylist-theme-saasland::partials.footer') + @endif
diff --git a/resources/themes/saasland/views/legacy-home-content.blade.php b/resources/themes/saasland/views/legacy-home-content.blade.php new file mode 100644 index 0000000..b093374 --- /dev/null +++ b/resources/themes/saasland/views/legacy-home-content.blade.php @@ -0,0 +1,61 @@ +
+
+
+ {{ $themeSettings['hero_eyebrow'] ?? 'A better way to run your business' }} +

{{ $heroTitle }}

+

{{ $heroText }}

+ + @if($enabled('show_metrics', true)) +
+
Performance overviewLive
+
Monthly revenue£48,240+18.4%
Active users12,842+9.2%
Conversion7.8%+2.1%
+
+
+ @endif +
+
+ +@if($enabled('show_features', true)) +
Powerful features

Everything your team needs to grow

Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.

+@foreach([['Smart automation','Remove repetitive work and delays.'],['Real-time analytics','Turn activity into useful business insight.'],['Content operations','Create, review and publish confidently.'],['Secure by design','Protect users and data with dependable controls.'],['Flexible integrations','Connect the tools your organisation already uses.'],['Modular architecture','Activate only the capabilities each site needs.']] as $i => $feature) +
{{ str_pad((string)($i+1),2,'0',STR_PAD_LEFT) }}

{{ $feature[0] }}

{{ $feature[1] }}

+@endforeach +
+@endif + +@if($enabled('show_pricing', true) && $pricingPlans->isNotEmpty()) +
Simple pricing

Choose a plan that fits

Plans are managed from Products & Pricing in the CMS.

+@foreach($pricingPlans as $plan) + +@endforeach +
+@endif + +@if($enabled('show_testimonials', true) && $testimonials->isNotEmpty()) +
Customer stories

Trusted by ambitious teams

@foreach($testimonials as $testimonial)
“{{ $testimonial->content }}”
{{ $testimonial->name }}@if($testimonial->company) · {{ $testimonial->company }}@endif
@endforeach
+@endif + +@if($enabled('show_team', false) && $teamMembers->isNotEmpty()) +
Our team

People behind the platform

@foreach($teamMembers as $member)

{{ $member->name }}

{{ $member->job_title }}

{{ $member->biography }}

@endforeach
+@endif + +@if($enabled('show_blog', true) && $latestBlogPosts->isNotEmpty()) +
Insights

Latest from the blog

@foreach($latestBlogPosts as $post)
{{ optional($post->published_at)->format('d M Y') }}

{{ $post->title }}

{{ $post->excerpt }}

Read article →
@endforeach
+@endif + +@if($enabled('show_news', true) && $latestNewsArticles->isNotEmpty()) +
Newsroom

Latest news

@foreach($latestNewsArticles as $article)
{{ optional($article->published_at)->format('d M Y') }}

{{ $article->title }}

{{ $article->excerpt }}

Read update →
@endforeach
+@endif + +@if($enabled('show_faq', true) && $faqs->isNotEmpty()) +

Answers before you begin

@foreach($faqs as $faq)

{{ $faq->answer }}

@endforeach
+@endif + +@if($enabled('show_newsletter', true)) +
+@endif + +

{{ $themeSettings['cta_title'] ?? 'Ready to transform your business?' }}

{{ $themeSettings['cta_description'] ?? 'Start building a faster, more connected operation today.' }}

@authOpen dashboard →@else @if(Route::has('register'))Get started free →@endif @endauth
diff --git a/resources/themes/saasland/views/legacy-home.blade.php b/resources/themes/saasland/views/legacy-home.blade.php new file mode 100644 index 0000000..405a28b --- /dev/null +++ b/resources/themes/saasland/views/legacy-home.blade.php @@ -0,0 +1,74 @@ +@extends('stylist-theme-saasland::layouts.app') + +@php + $title = $siteTitle; + $description = $siteDescription; + $hero = true; + $enabled = fn (string $key, bool $default = true) => filter_var($themeSettings[$key] ?? $default, FILTER_VALIDATE_BOOL); + $heroTitle = filled($themeSettings['hero_title'] ?? null) ? $themeSettings['hero_title'] : 'Transform Your Business With Our SaaS Solution'; + $heroText = filled($themeSettings['hero_description'] ?? null) ? $themeSettings['hero_description'] : 'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.'; +@endphp + +@section('content') +
+
+
+ {{ $themeSettings['hero_eyebrow'] ?? 'A better way to run your business' }} +

{{ $heroTitle }}

+

{{ $heroText }}

+ + @if($enabled('show_metrics', true)) +
+
Performance overviewLive
+
Monthly revenue£48,240+18.4%
Active users12,842+9.2%
Conversion7.8%+2.1%
+
+
+ @endif +
+
+ +@if($enabled('show_features', true)) +
Powerful features

Everything your team needs to grow

Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.

+@foreach([['Smart automation','Remove repetitive work and delays.'],['Real-time analytics','Turn activity into useful business insight.'],['Content operations','Create, review and publish confidently.'],['Secure by design','Protect users and data with dependable controls.'],['Flexible integrations','Connect the tools your organisation already uses.'],['Modular architecture','Activate only the capabilities each site needs.']] as $i => $feature) +
{{ str_pad((string)($i+1),2,'0',STR_PAD_LEFT) }}

{{ $feature[0] }}

{{ $feature[1] }}

+@endforeach +
+@endif + +@if($enabled('show_pricing', true) && $pricingPlans->isNotEmpty()) +
Simple pricing

Choose a plan that fits

Plans are managed from Products & Pricing in the CMS.

+@foreach($pricingPlans as $plan) + +@endforeach +
+@endif + +@if($enabled('show_testimonials', true) && $testimonials->isNotEmpty()) +
Customer stories

Trusted by ambitious teams

@foreach($testimonials as $testimonial)
“{{ $testimonial->content }}”
{{ $testimonial->name }}@if($testimonial->company) · {{ $testimonial->company }}@endif
@endforeach
+@endif + +@if($enabled('show_team', false) && $teamMembers->isNotEmpty()) +
Our team

People behind the platform

@foreach($teamMembers as $member)

{{ $member->name }}

{{ $member->job_title }}

{{ $member->biography }}

@endforeach
+@endif + +@if($enabled('show_blog', true) && $latestBlogPosts->isNotEmpty()) +
Insights

Latest from the blog

@foreach($latestBlogPosts as $post)
{{ optional($post->published_at)->format('d M Y') }}

{{ $post->title }}

{{ $post->excerpt }}

Read article →
@endforeach
+@endif + +@if($enabled('show_news', true) && $latestNewsArticles->isNotEmpty()) +
Newsroom

Latest news

@foreach($latestNewsArticles as $article)
{{ optional($article->published_at)->format('d M Y') }}

{{ $article->title }}

{{ $article->excerpt }}

Read update →
@endforeach
+@endif + +@if($enabled('show_faq', true) && $faqs->isNotEmpty()) +

Answers before you begin

@foreach($faqs as $faq)

{{ $faq->answer }}

@endforeach
+@endif + +@if($enabled('show_newsletter', true)) +
+@endif + +

{{ $themeSettings['cta_title'] ?? 'Ready to transform your business?' }}

{{ $themeSettings['cta_description'] ?? 'Start building a faster, more connected operation today.' }}

@authOpen dashboard →@else @if(Route::has('register'))Get started free →@endif @endauth
+@endsection