LandingPage Module Documentation
1. Introduction
The LandingPage module provides a comprehensive system for creating, managing, and customizing landing pages within your Laravel application. It features a dynamic section builder, theme support, menu management, and integration with Google Analytics. Its goal is to offer a flexible way to present content and can be extended to display data from other parts of your application.
2. Installation & Setup
Installation: It's assumed this module is installed via your application's main module management system (e.g., by uploading a ZIP in the admin panel or via Composer if it were a standalone package).
Migrations: After installation, run database migrations:
php artisan module:migrate LandingPage
Configuration:
- Homepage Slug: Define which page slug serves as the site's homepage via the
landingpage_homepage_slug setting (typically in your application's main settings table, defaulting to 'home'). This is managed under "Landing Page" → "Theme Settings" in the admin panel.
- Default Theme: The active theme is managed under "Landing Page" → "Theme Settings" in the admin panel.
3. Core Features
3.1. Page Management (LpPageController)
- CRUD Operations: Create, read, update, and delete landing pages.
- Content Fields: Each page has a title, slug (auto-generated or custom), main content (WYSIWYG editor for fallback/default content), and optional default hero section fields (title, subtitle, CTA).
- SEO Settings: Meta title, meta description, and meta keywords per page.
- Publishing: Pages can be in "Published" or "Draft" status.
- Sort Order: Control the order of pages.
- Theme Templates: Assign specific Blade templates from the active theme to individual pages for varied layouts.
3.2. Dynamic Section Builder (LpPageController, JavaScript)
Pages are constructed using dynamic sections. Admins can add, remove, and reorder sections on the page edit screen.
Available Section Types:
- Hero Section: Full-width banner with titles, subtitles, CTAs, and background images. Supports multiple slides.
- Services Overview: Display services with titles, descriptions, icons, or images.
- Team Members: Introduce team members with names, titles, images, and social links.
- Testimonials: Display customer quotes, authors, and author images.
- Contact CTA: A call-to-action section.
- Featured Companies: Highlights companies marked as featured from the
BusinessDirectory module. (Uses themes/job_board/sections/featured_companies.blade.php)
- Featured Products: Showcases products marked as featured from the
BusinessDirectory module. (Uses themes/job_board/sections/featured_products.blade.php)
- Featured Jobs: Displays job openings marked as featured from the
BusinessDirectory module. (Uses themes/job_board/sections/featured_jobs.blade.php)
- Featured Tenders: Lists tenders marked as featured from the
BusinessDirectory module. (Uses themes/job_board/sections/featured_tenders.blade.php)
- Legacy Integrations (can be adapted):
- Company Projects (Business Directory)
- Our Portfolio (DigitalVocano): Showcases your own company's projects from the
DigitalVocano module.
- Custom HTML: Allows embedding raw HTML content via a WYSIWYG editor.
Content Configuration:
Each section type has its own set of fields. Image uploads are supported for Hero slides, Services items, Team member photos, and Testimonial author images. The LpPageController::update() method handles storing these images in storage/app/public/landingpage/....
Frontend Rendering & Data Fetching (LandingPageSection Component & Section Partials):
The <x-landingpage::landing-page-section :section="$section" :activeTheme="$activeTheme" :businessData="$businessData" /> Blade component dynamically includes the appropriate Blade partial from the active theme's sections directory (e.g., themes/job_board/sections/hero.blade.php) based on the section's type. The Modules\LandingPage\View\Components\LandingPageSection.php class passes the section's content ($section->content) as $content to the partial. For sections requiring dynamic data (like "Featured Companies"), this data is typically fetched by FrontendPageController::getBusinessDataForPage() and passed to the main page view, then made available to the section partial. See "Data Flow for Featured Content Sections" for details.
3.4. Theme System (ThemeSettingController)
Themes are located in Modules/LandingPage/Resources/views/themes/. Each theme has its own subdirectory.
Theme Structure:
layouts/app.blade.php: Main layout file for most pages.
layouts/master.blade.php: (Optional) Alternative main layout, often used for specific page types like single product views if a different overall structure is needed.
partials/header.blade.php, partials/footer.blade.php: Reusable components.
sections/: Blade partials for each dynamic section type.
templates/: Optional directory for page-specific templates.
page.blade.php: Default view for rendering LpPage content and its sections.
home.blade.php: (Optional) A dedicated view for the homepage if it requires a structure different from page.blade.php.
page_not_found.blade.php: View for 404 errors.
- Assets (CSS, JS, Images): Typically managed via Vite and linked in
layouts/app.blade.php, but can also include theme-specific static assets.
The active theme is selected in the admin panel ("Landing Page" → "Theme Settings"). New themes, compatible with the DigitalVocano boilerplate, can be installed by uploading a ZIP package. The PublicPageController and LandingPageSection component use this setting for rendering.
Theme: job_board
The job_board theme is a specialized theme provided with this module. Its key files include:
- Layouts:
layouts/app.blade.php (main application layout with dark mode, SEO, Vite integration), layouts/master.blade.php (used by some single item templates).
- Partials:
partials/topbar.blade.php, partials/header.blade.php, partials/footer.blade.php.
- Page Renderers:
page.blade.php (default for LpPages), page_not_found.blade.php.
- Section Partials (
sections/):
hero.blade.php, services.blade.php, etc. (standard sections)
featured_companies.blade.php, featured_products.blade.php, featured_jobs.blade.php, featured_tenders.blade.php: These expect $content (from section settings) and a corresponding data variable (e.g., $featured_companies_data) passed from the controller via $businessData.
- Templates (
templates/): For single item views like company_single.blade.php, job_single.blade.php, product_single.blade.php, tender_single.blade.php, news_single.blade.php. Also includes listing pages for company-specific items: company_products_listing.blade.php, company_jobs_listing.blade.php, company_tenders_listing.blade.php.
3.5. Google Analytics Integration (ThemeSettingController, GoogleAnalyticsService)
Configuration:
In "Theme Settings", admins can:
- Enable/disable GA tracking.
- Enter GA4 Measurement ID (e.g.,
G-XXXXXXXXXX).
- Enter GA4 Property ID (numeric, for fetching stats).
- Provide the server path to the Google Service Account credentials JSON file.
- A "How-To" guide is included on the settings page.
Frontend Tracking:
If enabled, the GA4 gtag.js snippet is injected into the <head> of frontend pages.
Statistics Display:
- LandingPage Dashboard: Shows basic stats (Users, Page Views, Sessions) and Top Pages.
- Main Admin Dashboard: Conditionally displays the same GA summary if the module is active and GA is configured.
Data Fetching:
The Modules\LandingPage\Services\GoogleAnalyticsService.php class uses the google/analytics-data package to communicate with the Google Analytics Data API V1. It caches API responses.
New: Data Flow for Featured Content Sections
The "Featured" sections (e.g., Featured Companies, Featured Products) display dynamic content from other modules, typically items marked with an is_featured flag. Here's how the data flows:
- Admin Configuration:
An admin adds a "Featured Companies" section to an
LpPage via the page builder. They configure settings like the section title, number of items to show (e.g., items_to_show: 4). These settings are saved in the LpPageSection->content JSON field.
- Page Request & Controller Action:
A user visits the page. The request is handled by
FrontendPageController::show().
- Business Data Fetching:
FrontendPageController::show() calls its protected method getBusinessDataForPage(LpPage $page, string $activeTheme).
- Identifying Section Needs:
Inside
getBusinessDataForPage(), the code iterates through $page->sections. If it finds a section of type featured_companies, it proceeds to fetch the data.
// Simplified example from FrontendPageController::getBusinessDataForPage()
foreach ($page->sections as $section) {
if ($section->type === 'featured_companies' && !isset($data['featured_companies_data'])) {
$itemsToShow = (int) ($section->content['items_to_show'] ?? 4);
$data['featured_companies_data'] = \Modules\BusinessDirectory\Entities\Company::where('is_featured', true)
->where('status', 'approved')
->latest()->take($itemsToShow)->get();
}
// ... similar logic for other featured types ...
}
- Passing Data to View: The
$businessData array (now containing $featured_companies_data) is passed to the main page view (e.g., themes/job_board/page.blade.php) along with $page and $activeTheme.
- Section Component Rendering: The main page view iterates through
$page->sections and uses the <x-landingpage::landing-page-section :section="$section" :activeTheme="$activeTheme" :businessData="$businessData" /> component.
Important: The $businessData array must be passed to the component.
- Component Logic (
LandingPageSection.php): The render() method of the LandingPageSection component receives $section and $businessData. It determines the correct partial (e.g., featured_companies.blade.php). It makes $section->content available as $content to the partial. It also needs to extract the relevant data from $businessData (e.g., $businessData['featured_companies_data']) and pass it to the partial under the expected variable name (e.g., as $featured_companies_data).
// Simplified logic in LandingPageSection::render()
public function render()
{
$viewName = "landingpage::themes.{$this->activeTheme}.sections.{$this->section->type}";
$viewData = [
'section' => $this->section,
'content' => $this->section->content, // Already available as $content in partial
'activeTheme' => $this->activeTheme,
];
// Pass specific data from businessData based on section type
$dataKey = $this->section->type . '_data'; // e.g., 'featured_companies_data'
if (isset($this->businessData[$dataKey])) {
$viewData[$dataKey] = $this->businessData[$dataKey];
}
return view($viewName, $viewData);
}
- Partial Rendering: The section partial (e.g.,
themes/job_board/sections/featured_companies.blade.php) now has access to $content (for its settings) and $featured_companies_data (for the items to display) and renders the HTML.
4. How the LandingPage Module Interacts with Other Data/Modules
The LandingPage module is primarily a content presentation system. It can be configured to display data from other modules.
4.1. Integrating External Module Data into Dynamic Sections
-
Define a New Section Type:
Add a unique key and display name to
$sectionTypes in LpPageController@edit.
-
Admin Configuration Fields:
In
edit.blade.php (JavaScript renderContentFields function), add a case for your new section type and generate HTML for its admin fields (e.g., multi-selects, number inputs).
-
Data Saving:
The configuration from the admin fields (e.g., section title, number of items) is saved in the section's JSON
content by LpPageController@update.
-
Create Frontend Rendering Partial:
In your active theme (e.g.,
themes/general/sections/your_section_type.blade.php), create a new Blade file. This partial receives $section and its $content.
For simple sections, you might query directly in the partial. However, for more complex data or to keep partials cleaner, it's recommended to fetch data in FrontendPageController::getBusinessDataForPage() and pass it via $businessData as described in the "Data Flow for Featured Content Sections" above.
<!-- Example: themes/general/sections/portfolio-projects.blade.php -->
@php
$sectionTitle = $section->content['title'] ?? 'Our Recent Work';
$numberOfProjects = (int)($section->content['number_of_items'] ?? 3);
$portfolioProjects = \Modules\DigitalVocano\Entities\Project::query()
// ->where('is_published', true) // Example criteria
// ->orderBy('sort_order', 'asc')
// ->take($numberOfProjects)
// ->get();
// Recommended: $portfolioProjects = $portfolio_projects_data ?? collect(); (assuming data passed via $businessData)
@endphp
@if($portfolioProjects->isNotEmpty())
<section class="portfolio-section">
<h2>{{ $sectionTitle }}</h2>
<div class="project-grid">
@foreach($portfolioProjects as $dvProject)
<div class="project-card">
<h3>{{ $dvProject->name }}</h3>
<!-- ... more project details ... -->
</div>
@endforeach
</div>
</section>
@endif
-
Automatic Rendering: The
<x-landingpage::landing-page-section> component automatically includes your new section partial based on its type.
Key Principle: The LandingPage module stores references or configurations for displaying data from other modules. The actual data fetching from those other modules typically happens in FrontendPageController::getBusinessDataForPage() and is then passed to the theme's specific section partials for rendering.
4.2. Creating New Dedicated Listing Pages (e.g., /jobs, /portfolio)
To create entirely new pages that list all items of a certain type from another module (e.g., a page at /jobs to show all job listings), follow these steps:
-
Define a Route:
In
Modules/LandingPage/routes/web.php, add a new route. Make sure it's placed before the generic /{slug?} catch-all route.
// Example for a new "/portfolio" page
Route::get('/portfolio', [PublicPageController::class, 'showPortfolioProjects'])->name('landingpage.portfolio.index');
-
Create a Controller Method:
In
Modules/LandingPage/Http/Controllers/PublicPageController.php (for general listings like all jobs, all products) or Modules/LandingPage/Http/Controllers/FrontendController.php (for company-specific listings like all products of a specific company), add a new public method. This method will:
- Fetch the required data from the other module's entities (e.g.,
\Modules\DigitalVocano\Entities\Project::where('is_published', true)->paginate(10);).
- Pass any necessary data (like page title, meta description, and the fetched items) to a new view.
// Example method in PublicPageController.php for a general portfolio page
public function showPortfolioProjects(Request $request)
{
$activeTheme = setting('landingpage_active_theme', 'general');
$portfolioProjects = \Modules\DigitalVocano\Entities\Project::where('is_published', true)->orderBy('sort_order', 'asc')->paginate(9);
$pageTitle = 'Our Portfolio';
return view("landingpage::themes.{$activeTheme}.portfolio", compact('portfolioProjects', 'pageTitle', 'activeTheme'));
}
// Example method in FrontendController.php for company-specific products
public function showCompanyProducts($company_slug)
{
$company = \Modules\BusinessDirectory\Entities\Company::where('slug', $company_slug)->firstOrFail();
$activeTheme = setting('landingpage_active_theme', 'general');
$products = $company->products()->where('status', 'published')->paginate(12);
$page = (object) ['title' => 'Products from ' . $company->name, /* ... other meta ... */];
return view("landingpage::themes.{$activeTheme}.templates.company_products_listing", compact('company', 'products', 'page', 'activeTheme'));
}
-
Create a Blade View:
In your active theme's directory (e.g.,
Modules/LandingPage/Resources/views/themes/job_board/ or themes/job_board/templates/), create a new Blade file for your page (e.g., portfolio.blade.php or templates/company_products_listing.blade.php). This view will extend your theme's layout and display the fetched items, including pagination links if used.
-
(Optional) Add to Menu:
Use the Menu Management system in the admin panel to add a link to your new page (e.g., using the route name
landingpage.portfolio.index or the direct URL /portfolio).
4.3. Summary of Implemented Integrations:
Following the general approach, the module now integrates with:
BusinessDirectory Module:
- Featured Companies Section: Displays featured
Company entities. (Partial: sections/featured_companies.blade.php)
- Featured Products Section: Displays featured
Product entities. (Partial: sections/featured_products.blade.php)
- Featured Jobs Section: Displays featured
Job entities. (Partial: sections/featured_jobs.blade.php)
- Featured Tenders Section: Displays featured
Tender entities. (Partial: sections/featured_tenders.blade.php)
- Company Profile Page (
theme.company.show): Displays details of a single company, including its products, jobs, and tenders. (Template: templates/company_single.blade.php or BusinessDirectory::frontend.companies.profile)
- "View All" Company Items: Pages to list all products (
theme.company.products), jobs (theme.company.jobs), or tenders (theme.company.tenders) for a specific company. (Templates: templates/company_products_listing.blade.php, etc.)
DigitalVocano Module:
- Our Portfolio: Fetches from
Modules\DigitalVocano\Entities\Project.
Dedicated listing pages for general content (e.g., all jobs via /jobs, all products via /products) are handled by PublicPageController.php and corresponding theme views (e.g., themes/job_board/jobs.blade.php - if created).
5. Customization & Development
5.1. Creating New Themes
- Develop your theme, ideally based on or compatible with the DigitalVocano boilerplate to ensure structural consistency.
- Modify
layouts/app.blade.php, partials/header.blade.php, partials/footer.blade.php.
- Customize CSS (e.g., in
themes/my-new-theme/css/theme.css or integrate with Vite).
- Override or create new section partials in
themes/my-new-theme/sections/.
- Package your theme files (including a
theme.json manifest if required by the installer) into a ZIP file.
- Install the theme via the "Theme Settings" page in the LandingPage module's admin section by uploading the ZIP file.
5.2. Creating New Section Types
- Define Type Key: Choose a unique string (e.g.,
image-gallery).
- Admin Fields: Add a
case in the renderContentFields JavaScript function (admin/pages/edit.blade.php) to define HTML for input fields.
- Data Handling (Controller): If special data processing is needed (e.g., image uploads), update
LpPageController@update.
- Frontend Partial: Create a Blade file (e.g.,
image-gallery.blade.php) in themes/[your-active-theme]/sections/. This file receives $section and $content.
6. Important Files & Controllers
Controllers (Admin):
Modules/LandingPage/Http/Controllers/Admin/LpPageController.php
Modules/LandingPage/Http/Controllers/Admin/MenuController.php
Modules/LandingPage/Http/Controllers/Admin/MenuItemController.php
Modules/LandingPage/Http/Controllers/Admin/ThemeSettingController.php
Controller (Frontend):
Modules/LandingPage/Http/Controllers/FrontendPageController.php: Responsible for rendering LpPage entities (dynamic pages built with sections). It determines the active theme, fetches the LpPage model, and calls getBusinessDataForPage() to gather any additional data required by the page's sections (like featured items). It then selects the appropriate theme view (e.g., page.blade.php or a custom template) for rendering.
Modules/LandingPage/Http/Controllers/FrontendController.php: Handles displaying single detail pages for entities like individual jobs (showJob), companies (showCompany), products (showProduct), tenders (showTender), and news articles (showNewsArticle). It also manages the "View All" listing pages for items belonging to a specific company (e.g., showCompanyProducts, showCompanyJobs).
Modules/LandingPage/Http/Controllers/PublicPageController.php: (Legacy/Alternative) Can be used for general listing pages that are not tied to a specific LpPage entity, such as a page listing all jobs (/jobs) or all products (/products) across the site. The current web.php routes point these general listing pages to this controller.
Services:
Modules/LandingPage/Services/GoogleAnalyticsService.php
Entities (Models):
Modules/LandingPage/Entities/LpPage.php
Modules/LandingPage/Entities/LpPageSection.php
Modules/LandingPage/Entities/Menu.php
Modules/LandingPage/Entities/MenuItem.php
Core Views (Admin):
Modules/LandingPage/resources/views/admin/pages/index.blade.php (Dashboard & List)
Modules/LandingPage/resources/views/admin/pages/edit.blade.php (Page Builder)
Modules/LandingPage/resources/views/admin/menus/edit.blade.php (Menu Management)
Modules/LandingPage/resources/views/admin/theme_settings/edit.blade.php (Theme & GA Settings)
Core Views (Frontend - Theme Specific):
Example for job_board theme:
themes/job_board/layouts/app.blade.php (Main Layout)
themes/job_board/page.blade.php (Default renderer for LpPages)
themes/job_board/partials/ (header.blade.php, footer.blade.php, topbar.blade.php)
themes/job_board/sections/ (hero.blade.php, featured_companies.blade.php, etc.)
themes/job_board/templates/ (company_single.blade.php, job_single.blade.php, company_products_listing.blade.php, etc.)
Blade Component:
- Class:
Modules/LandingPage/View/Components/LandingPageSection.php
- Invoked as:
<x-landingpage::landing-page-section :section="$section" :activeTheme="$activeTheme" :businessData="$businessData" />
- This component is responsible for:
- Determining the correct Blade partial for the section based on
$section->type and $activeTheme.
- Passing
$section->content as $content to the partial.
- Passing the relevant data from the
$businessData array to the partial (e.g., $featured_companies_data).
7. Deep Dive: The Hero Section
The "Hero Section" is a prominent feature, designed for strong visual impact at the top of landing pages. It supports multiple slides, each configurable with:
- Slide Title: Main heading.
- Slide Subtitle: Supporting text.
- CTA Text & Link: Call to action button.
- Background Image: Supports direct uploads.
On the frontend, it's typically rendered using a JavaScript slider library (like Swiper.js) within the theme's sections/hero.blade.php partial.
The LpPage model also has "Default Hero Section" fields (hero_title, hero_subtitle, etc.) which can serve as a fallback if no dynamic "Hero Section" is added to a page, or if the theme is designed to use them.
Note: The appearance and behavior of the Hero Section depend heavily on the HTML and CSS in your active theme's hero.blade.php partial.
8. Google Analytics Setup Summary
To enable Google Analytics statistics display, you need to configure it correctly:
-
Google Cloud Project & API:
Create/select a project in Google Cloud Console and enable the "Google Analytics Data API".
-
Service Account:
Create a Service Account, grant it the "Analytics Reader" role, and download its JSON key.
-
Add Service Account to GA4:
In your GA4 Property settings, add the service account's email as a user with "Viewer" permissions.
-
Store Credentials & Configure Path:
Securely store the downloaded JSON key file on your server (e.g.,
storage/app/google-analytics/service-account-credentials.json).
Important: Add this path to your .gitignore.
Enter the full, absolute path to this JSON file in the "Service Account Credentials JSON Path" field in the LandingPage Theme Settings.
-
Enter IDs:
Fill in your "Google Analytics Measurement ID" (e.g., G-XXXXXXXXXX) and "Google Analytics Property ID" (numeric) in the Theme Settings.
Detailed steps are available on the "Theme Settings" page within the admin panel of the LandingPage module.
9. Composer Dependencies
For Google Analytics data fetching, this module relies on the google/analytics-data package. Ensure it's installed in your main project:
composer require google/analytics-data
The module's own composer.json (Modules/LandingPage/composer.json) primarily defines its PSR-4 autoloading and doesn't list external dependencies, as those are expected to be managed by the main application's composer.json.
10. Routing
Frontend Routes (Modules/LandingPage/routes/web.php):
// General listing pages (handled by PublicPageController)
Route::get('/jobs', [PublicPageController::class, 'showJobs'])->name('landingpage.jobs.index');
Route::get('/products', [PublicPageController::class, 'showProducts'])->name('landingpage.products.index');
Route::get('/tenders', [PublicPageController::class, 'showTenders'])->name('landingpage.tenders.index');
Route::get('/companies', [PublicPageController::class, 'showCompanies'])->name('landingpage.companies.index');
Route::get('/portfolio', [PublicPageController::class, 'showPortfolioProjects'])->name('landingpage.portfolio.index');
Route::get('/news', [PublicPageController::class, 'showNews'])->name('landingpage.news.index');
// Single item detail pages (handled by FrontendController)
Route::get('/jobs/{slug}', [FrontendController::class, 'showJob'])->name('theme.job.show');
Route::get('/companies/{slug}', [FrontendController::class, 'showCompany'])->name('theme.company.show');
Route::get('/products/{slug}', [FrontendController::class, 'showProduct'])->name('theme.product.show');
Route::get('/tenders/{slug}', [FrontendController::class, 'showTender'])->name('theme.tender.show');
Route::get('/news/{slug}', [FrontendController::class, 'showNewsArticle'])->name('theme.news.show');
// "View All" pages for specific company's items (handled by FrontendController)
Route::get('/company/{company_slug}/products', [FrontendController::class, 'showCompanyProducts'])->name('theme.company.products');
Route::get('/company/{company_slug}/jobs', [FrontendController::class, 'showCompanyJobs'])->name('theme.company.jobs');
Route::get('/company/{company_slug}/tenders', [FrontendController::class, 'showCompanyTenders'])->name('theme.company.tenders');
// Catch-all for dynamic pages
Route::get('/{slug?}', [PublicPageController::class, 'showPage'])
->where('slug', '(.*)')
->name('landingpage.page.show');
This is a catch-all route and should ideally be one of the last routes registered in your application to avoid conflicts.
The FrontendPageController is responsible for handling the catch-all /{slug?} route to display dynamic LpPage entities. The PublicPageController handles predefined listing pages like /jobs. The FrontendController handles single item detail views and company-specific listings.
Admin Routes (Modules/LandingPage/routes/admin.php):
These routes are typically prefixed with /admin/landingpage/ (or similar, depending on your main application's admin route group).
Route::resource('pages', LpPageController::class);
Route::get('theme-settings', [ThemeSettingController::class, 'edit'])->name('theme.settings.edit');
Route::put('theme-settings', [ThemeSettingController::class, 'update'])->name('theme.settings.update');
Route::resource('menus', MenuController::class);
Route::post('menus/{menu}/items', [MenuItemController::class, 'store'])->name('menus.items.store');
Route::put('menus/{menu}/items/{item}', [MenuItemController::class, 'update'])->name('menus.items.update');
Route::delete('menus/{menu}/items/{item}', [MenuItemController::class, 'destroy'])->name('menus.items.destroy');
Route::post('menus/{menu}/items/reorder', [MenuItemController::class, 'reorder'])->name('menus.items.reorder');