Imported from houssembaklouty/softyfact-public (
AGENTS.md). Install upstream withnpx skills add houssembaklouty/softyfact-public. Copyright stays with the author.
AGENTS.md — SoftyFact Public
Architecture
- Lean Blade marketing site for a Tunisian invoicing SaaS. Not an SPA — no Vue/React components, no Inertia pages (despite
config/inertia.phpexisting, it's unused). - Stack: Laravel 11 · PHP 8.2 · Blade · Alpine.js 3.14 · Tailwind CSS 3 · Vite 5
- Database: SQLite (local/Docker), MySQL 8 (production)
- Two-app system: This site forwards orders/leads via API to the core ERP (
fetora-pro) atconfig('app.core_app_url'), authenticated withPUBLIC_SITE_API_KEY - Bilingual: French (default) + Arabic (RTL). All locale-aware via
__('key')fromlang/fr.jsonandlang/ar.json. Blog content uses per-locale model accessors (getTitle(),getBody()) - No auth: All routes public, no users table
Commands
docker compose up -d # Local dev on port 8083
docker compose exec app php artisan migrate --seed
npm run dev # Vite dev server (assets)
npm run build # Production bundle → public/build/
composer install
php artisan test # PHPUnit 11 (no tests written yet)
./vendor/bin/pint # Laravel Pint (PSR-12)
# Production deploy
bash deploy/deploy-public.sh # Rsync → composer install → npm build → migrate → cache
Conventions
- Blade-only rendering: All pages extend
resources/views/app.blade.php. Interactivity via Alpine.js (x-data,x-show,@click) - Translation keys: CamelCase (
heroTitle,blogSubtitle). Must add bothfr.jsonandar.jsonentries for new text - Controller pattern: Validate input → forward to core API via
Http::withToken()→ return view. Controllers hold no business logic - A/B testing: Homepage only. Middleware sets 30-day cookie
ab_variant(A/B). Override via?variant=A. Translation keys for variant B suffixed withB(e.g.heroTitleB) - Rate limits: Orders 10/min, leads 5/min (defined in
routes/web.php) - SEO: Every page has
@section('meta')with title, description, OG tags, canonical. Blog posts include JSON-LD structured data - Colors: Tailwind config uses a Material Design 3 palette (
cm-*tokens likecm-primary,cm-surface,cm-on-surface) - Pricing: Cloud-only. Price read from
config('app.order_amount_online')with default 119 TND/year. Never hardcode - A/B testing variant pages: Variant B's homepage CTA links to
/product/buyonepay(product-offline-2.blade.php) — a standalone landing page (does NOT extendapp.blade.php— it has its own<head>and CDN-loaded assets).
Gotchas
- No tests directory exists yet (no
phpunit.xmleither). Test infrastructure needs bootstrapping before runningphp artisan test - Inertia config is a red herring —
config/inertia.phpexists but SSR is disabled and no Inertia pages are used - Bilingual blog model:
BlogPostexposesgetTitleAttribute(),getBodyAttribute(), etc. that auto-select locale-aware columns (title_frvstitle_ar). Always use accessors, never raw column access - Cover images: Blog cover defaults are SVG gradients defined in
BlogPost::getCoverMeta(). Real photos can be downloaded viaphp artisan blog:generate-covers(needsPEXELS_API_KEY) - OG images: Generated via
php artisan blog:generate-og-images(GD-based, no ImageMagick) - Docker port: App runs on 8083 in docker compose (not default 80/8080)
- No CI, no pre-commit hooks, no Makefile
- RTL: Arabic pages need RTL CSS. Tailwind RTL utilities are in
resources/css/app.css