Imported from derdjilali/ttttt (
AGENTS.md). Install upstream withnpx skills add derdjilali/ttttt. Copyright stays with the author.
AGENTS.md — Djezzy CMS Backend
Quick Facts
- Stack: Laravel 12 + Filament 3.2 (admin CMS) + Vite + PostgreSQL
- Language: PHP 8.3.26+
- Purpose: Multi-language (AR/EN/FR) widget-based CMS for dynamic content
- Key pattern: Reusable scoped Blade widgets with JSON content + Filament form schemas
Essential Commands
Development
php artisan serve # Start dev server (http://127.0.0.1:8000)
npm run dev # Run Vite in watch mode
php artisan tinker # Laravel REPL for quick DB queries
Testing & Validation
- no need for testing
Database
php artisan migrate:fresh # Wipe & re-run all migrations (dev only!)
php artisan migrate:status # Show migration state
php artisan db:seed # Run seeders (if defined)
Filament Admin
- Access at
/admafterphp artisan serve - Must authenticate; uses Filament's built-in auth (model:
User)
Database & Models
Key models in app/Models/:
Widget← Core: stores widget instances with JSON content (indexed byname)Widgettype← defines widget schemas (1:M with Widget)Widgetlayout← groups widgets on pagesPage← site pages with SEO fields, soft deletesMedia← file/image metadataMenuItem,Menu,SubmenuItem← navigationForm,FormSubmission,WidgetFormResult← form handling
DB Connection: PostgreSQL (configured in .env as djezzy_main_db)
Multi-language pattern: Tables have _ar, _en, _fr suffixed columns (e.g., title_ar, title_en, title_fr)
Widget System (Core Pattern)
Blade Templates
Location: resources/views/widgets/*.blade.php
Essential structure (always includes):
@php
$widgetId = 'widget-' . $id; // Prevent CSS/JS conflicts
@endphp
<style>
#{{ $widgetId }} .class-name { /* scoped styles */ }
{!! $customcss !!} // Filament-injected custom CSS
</style>
{!! $before !!}
<div id="{{ $widgetId }}">
<!-- widget HTML -->
@foreach ($content['key'] ?? [] as $item)
<!-- use @isset() for optional sections -->
@endforeach
</div>
{!! $after !!}
<script>
document.addEventListener("DOMContentLoaded", function() {
const widget = document.getElementById("{{ $widgetId }}");
if (!widget) return;
// all DOM queries scoped to widget
});
</script>
Key patterns:
- Always wrap content keys in
$content['key'] ?? []and@isset()checks - Asset paths:
asset(collect($content['img'] ?? [])->first()) - All IDs/classes scoped with
#{{ $widgetId }} - Never use global
document.querySelector(); always scope to widget div
Filament Schema & JSON
Location: app/Filament/Resources/WidgetRessourceResource.php (lines ~100-1378)
Helper methods (in WidgetRessourceResource):
static::fileUploadField($name, $label)→ FileUpload with diskpublic_uploads/widgetsstatic::videoUploadField($name, $label)→ FileUpload for videos, 20MB limit
JSON content structure:
- Stored as
jsoncolumn inwidgetstable - Keys must match Blade template
$content['key']references - File uploads store as UUID-keyed objects:
{"uuid": "path/to/file"} - Example:
{ "title": "My Widget", "icon": { "abc123": "widgets/icon.svg" }, "cards": [{ "title": "...", "description": "..." }] }
Critical Gotchas & Non-Obvious Quirks
1. Asset File Paths
- Upload disk is
public_uploads(config:config/filesystems.php) - Directory auto-set to
widgets/in fileUploadField helper - Access in Blade:
asset(collect($content['field'] ?? [])->first()) - Never hardcode paths; always use
asset()helper
2. RTL & Multi-Language
- CSS scope must include RTL:
[dir='rtl'] #{{ $widgetId }} .selector - Language set via APP_LOCALE (currently
frin.env) - Widgets render different columns per language in Admin
3. Widget ID Conflicts
- All
id=,for=,aria-*,data-bs-target=must include{{ $widgetId }} - Prevents conflicts when same widget renders multiple times
- Example:
id="{{ $widgetId }}-tab-{{ $loop->index }}"
4. Filament File Upload Format
- Multiple files stored as JSON object:
{"uuid": "path", "uuid2": "path2"} - Use
collect($content['field'] ?? [])->first()to get first file - Use
foreach ($content['field'] ?? [] as $file)to iterate all
5. Bootstrap 5 Integration
- Included by default; use
data-bs-*attributes - Swiper.js loaded separately in widget templates if needed
- ALWAYS scope Swiper instances to widget div
File Organization
app/
Filament/
Resources/
WidgetRessourceResource.php ← Main widget CMS schema
WidgetRessourceResource/Pages/ ← Create/Edit/List pages
Schemas/MobileOffers/ ← Widget-specific schemas
Fields/ ← Custom form fields
Models/ ← 26 models (Widget, Page, etc.)
Http/Controllers/ ← API endpoints
Repositories/ ← Data access layer
resources/
views/
widgets/ ← Blade templates (main--*.blade.php)
layouts/ ← Page layouts
filament/ ← Filament customizations
css/filament/ ← Filament-specific CSS
js/filament/ ← Filament-specific JS
config/
cmsconfig.php ← Custom CMS config
filament.php ← Filament settings
filesystems.php ← Disk definitions (public_uploads)
database/
migrations/ ← All timestamped; recently added SEO/i18n columns
Development Workflow
Creating a New Widget
-
Create Blade template:
resources/views/widgets/my-widget.blade.php- Use scoped ID pattern, follow existing template structure
-
Add Filament schema: Add array to
WidgetRessourceResource::form()(or separate schema class)- Use
fileUploadField()andvideoUploadField()helpers - Include sensible default values
- Use
-
Create sample JSON: Test in Filament Admin to verify JSON structure
-
Register in Widgettype: Create/update
Widgettyperecord in Admin pointing to your Blade file -
Test rendering: Create
Widgetinstance in Admin, assign to page, verify on frontend
Common Mistakes (Catch Before Commit)
- ❌ Unscoped CSS selectors (no
#{{ $widgetId }}) - ❌ Global JS
document.querySelector()(must scope to widget) - ❌ Missing
@isset()on optional content fields - ❌ Hardcoded asset paths instead of
asset() - ❌ Static IDs instead of
{{ $widgetId }}-suffix - ❌ File uploads not wrapped in
collect(...)->first()
Configuration Files (Do Not Skip)
| File | Purpose | Edit Caution |
|---|---|---|
.env |
DB credentials, app locale (currently fr) |
Never commit! Use .env.example |
config/filesystems.php |
Disk definitions (public_uploads) | Verify before changing upload logic |
config/filament.php |
Admin panel settings | Leave defaults unless extending features |
config/cmsconfig.php |
Custom CMS constants | Add here, not in code |
phpunit.xml |
Test DB settings (SQLite in-memory) | Usually safe; ensure test env vars match |
Performance & Caching
- Blade caching: Laravel caches compiled Blade views in
storage/framework/views/ - Config caching:
php artisan config:cache(run before deploy) - Asset caching: Vite handles hashing; manifest in
public/build/ - DB: PostgreSQL connection pooling configured in
.env
Common Issues & Solutions
| Symptom | Likely Cause | Fix |
|---|---|---|
| Widget CSS not scoped | Missing #{{ $widgetId }} |
Add widget ID prefix to all selectors |
| File upload returns null | Not using collect($content['field'] ?? [])->first() |
Use helper correctly |
| JS errors on page | Swiper/Bootstrap not scoped to widget | Wrap in document.addEventListener("DOMContentLoaded") and scope queries |
| RTL text looks wrong | Missing RTL CSS rule | Add [dir='rtl'] #{{ $widgetId }} selectors |
| Tests fail silently | SQLite in-memory DB not migrated | Ensure phpunit.xml has <env name="DB_DATABASE" value=":memory:"/> |
| Filament custom CSS not applied | Not inserting {!! $customcss !!} in widget |
Add to <style> tag or create one |
Git workflow: Commits follow pattern <type>: <description> (e.g., feat: add mobile offers widget, fix: scope swiper JS)
References
- Blade docs: Laravel 12 views, component scoping
- Filament docs: Form builder, resource pages, custom fields
- PostgreSQL config:
config/database.php(connection pooling, PDO settings) - Widget template examples:
resources/views/widgets/main--services.blade.php