diff --git a/.env.local-example b/.env.local-example
index 789b5f1663ac09c686c9496474a848dfbfab53a1..004ebc7adb39a9800ad2126fb1989674b8ce6005 100644
--- a/.env.local-example
+++ b/.env.local-example
@@ -82,4 +82,8 @@ CDN_FILESYSTEM_DISK=bunnycdn
 AI_PROVIDER=openai
 OPENAI_URL=https://api.openai.com/v1/chat/completions
 OPENAI_API_KEY=
-OPENAI_MODEL=gpt-4o-mini
\ No newline at end of file
+OPENAI_MODEL=gpt-4o-mini
+
+IP_ADDRESS_RESOLVER_URL=http://ip-api.com/batch
+IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE=15
+IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL=100
\ No newline at end of file
diff --git a/.env.production b/.env.production
index 4306866dbd91a2ba10c3c8c77423bafcc8dbce5b..31ad2117f53d2b40e76814c23386cfdab406ec38 100644
--- a/.env.production
+++ b/.env.production
@@ -82,4 +82,10 @@ CDN_FILESYSTEM_DISK=bunnycdn
 AI_PROVIDER=openai
 OPENAI_URL=https://api.openai.com/v1/chat/completions
 OPENAI_API_KEY=
-OPENAI_MODEL=gpt-4o-mini
\ No newline at end of file
+OPENAI_MODEL=gpt-4o-mini
+
+IP_ADDRESS_RESOLVER_URL=http://ip-api.com/batch
+IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE=15
+IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL=100
+
+APP_PRIVATE_MODE_SECRET=secret
\ No newline at end of file
diff --git a/.env.testing b/.env.testing
index 27f3f5951fe626efb7e765dee1da5748abae4a5d..7e40aa8641d219fb6f21e09ad7e1760f43b7c0fe 100644
--- a/.env.testing
+++ b/.env.testing
@@ -78,4 +78,10 @@ BUNNYCDN_REGION=de
 AI_PROVIDER=openai
 OPENAI_URL=https://api.test-provider.com/v1/chat/completions
 OPENAI_API_KEY=fake-api-key
-OPENAI_MODEL=gpt-4o-mini
\ No newline at end of file
+OPENAI_MODEL=gpt-4o-mini
+
+IP_ADDRESS_RESOLVER_URL=http://api.test-provider.com/batch
+IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE=15
+IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL=100
+
+APP_PRIVATE_MODE_SECRET=secret
\ No newline at end of file
diff --git a/README.md b/README.md
index 3baed05eaefdb5e364186c5bead5128a60991f39..f60d50dcaa41906164541649d954ffbeb58c9f55 100644
--- a/README.md
+++ b/README.md
@@ -62,25 +62,6 @@ #### Laravel Idea
 
 #### Configuration
 
-##### Configuration des projets multiples
-
-Comme le projet est dépendant de quelques paquets développés en interne, il est nécessaire de configurer PHPStorm pour
-vous faciliter leur développement en simultané.
-
-1. Ouvrez les préférences de PHPStorm (Ctrl + Alt + S)
-2. Allez dans `Version Control`
-3. Allez dans `Directory Mappings`
-4. Cliquez sur `+` puis `Directory`
-5. Sélectionnez le dossier du projet sous-jacent
-6. Cliquez sur `OK`
-
-![Projets Git Multiples](a1readme-assets/multi-vcs.jpg)
-
-Vous devriez voir apparaître les multiples projets Git dans l'onglet vcs de PHPStorm. À présent, vous devriez pouvoir
-créer des branches sur les différents projets simultanément.
-
-![Menu VCS](a1readme-assets/vcs-menu.jpg)
-
 ##### Interpréteur PHP
 
 Il est nécessaire de renseigner le container Docker comme interpréteur PHP. Pour cela, suivez les étapes suivantes :
diff --git a/app/Console/Commands/ProcessIpAdressesCommand.php b/app/Console/Commands/ProcessIpAdressesCommand.php
new file mode 100644
index 0000000000000000000000000000000000000000..c9f8b9e07e5ca2c41dfaa11a1386b3115a4f2d3e
--- /dev/null
+++ b/app/Console/Commands/ProcessIpAdressesCommand.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Jobs\ProcessIpAddressesJob;
+use Illuminate\Console\Command;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+
+class ProcessIpAdressesCommand extends Command
+{
+    protected $signature = 'process:ip-adresses';
+
+    protected $description = 'Process ip adresses to resolve their location';
+
+    public function handle(): void
+    {
+        $ipAddresses = IpAddress::leftJoin('ip_address_metadata', 'ip_addresses.id', '=', 'ip_address_metadata.ip_address_id')
+            ->whereNull('ip_address_metadata.id')
+            ->select('ip_addresses.id', 'ip_addresses.ip')
+            ->get();
+
+        if ($ipAddresses->isEmpty()) {
+            $this->info('No ip adresses to process');
+
+            return;
+        }
+
+        $this->info("Processing {$ipAddresses->count()} ip adresses");
+        ProcessIpAddressesJob::dispatch($ipAddresses);
+    }
+}
diff --git a/app/Http/Controllers/Admin/CreationController.php b/app/Http/Controllers/Admin/CreationController.php
index 93f4590310abaaf1fb1c4b4a49123722506eaa34..792b41cf0547811b75577778e475c6e0b8d6b052 100644
--- a/app/Http/Controllers/Admin/CreationController.php
+++ b/app/Http/Controllers/Admin/CreationController.php
@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Http\Controllers\Controller;
+use App\Jobs\TranslateCreationJob;
 use App\Models\Category;
 use App\Models\Creation;
 use App\Models\Translation;
@@ -350,4 +351,15 @@ public function removeAdditionalImage(int $creationId, int $uploadedPictureId):
             'success' => true,
         ]);
     }
+
+    public function translateWithAi(int $creationId): JsonResponse
+    {
+        $creation = Creation::findOrFail($creationId);
+        $job = new TranslateCreationJob($creation);
+        dispatch($job);
+
+        return response()->json([
+            'success' => true,
+        ]);
+    }
 }
diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php
index eca59cfbc44ca3fb6f686ab7948e98eb8745976a..6c119307d9c3c4fd5631c82e04315cb4acaa8e4f 100644
--- a/app/Http/Controllers/Admin/HomeController.php
+++ b/app/Http/Controllers/Admin/HomeController.php
@@ -4,6 +4,7 @@
 
 use App\Http\Controllers\Controller;
 use Carbon\Carbon;
+use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Route;
 use Illuminate\Support\Str;
 use Illuminate\View\View;
@@ -11,29 +12,43 @@
 
 class HomeController extends Controller
 {
-    public function index(): View
+    public function index(Request $request): View
     {
-        $publicRoutes = collect(Route::getRoutes()->getRoutes())
-            ->filter(fn ($route) => ! Str::startsWith($route->uri, ['admin', 'api']) && ! empty($route->uri) && $route->uri !== '/')
-            ->pluck('uri');
-
-        $visits = LoggedRequest::selectRaw('
-                logged_requests.url_id,
-                logged_requests.ip_address_id,
-                logged_requests.country_code,
-                logged_requests.created_at,
-                urls.url
-            ')
+        $request->validate([
+            'start_date' => 'nullable|date',
+            'end_date' => 'nullable|date',
+        ]);
+
+        $routes = Route::getRoutes()->getRoutes();
+
+        $individualExcludedRoutes = [];
+        foreach ($routes as $route) {
+            if (Str::startsWith($route->uri, ['login', 'register', 'password', 'admin', 'forgot-password'])) {
+                $individualExcludedRoutes[] = config('app.url').'/'.$route->uri;
+            }
+        }
+
+        $visits = LoggedRequest::select([
+            'logged_requests.url_id',
+            'logged_requests.referer_url_id',
+            'logged_requests.origin_url_id',
+            'logged_requests.ip_address_id',
+            'ip_address_metadata.country_code',
+            'logged_requests.created_at',
+            'urls.url',
+            'referer_url.url as referer_url',
+            'origin_url.url as origin_url'])
             ->distinct('logged_requests.url_id', 'logged_requests.ip_address_id')
             ->join('urls', 'logged_requests.url_id', '=', 'urls.id')
+            ->leftJoin('urls as referer_url', 'logged_requests.referer_url_id', '=', 'referer_url.id')
+            ->leftJoin('urls as origin_url', 'logged_requests.origin_url_id', '=', 'origin_url.id')
             ->join('user_agent_metadata', 'logged_requests.user_agent_id', '=', 'user_agent_metadata.user_agent_id')
-            ->where(function ($query) use ($publicRoutes) {
-                foreach ($publicRoutes as $uri) {
-                    $query->orWhere('urls.url', 'like', "%$uri%");
-                }
-            })
+            ->join('ip_address_metadata', 'logged_requests.ip_address_id', '=', 'ip_address_metadata.ip_address_id')
+            ->whereLike('urls.url', config('app.url').'%')
+            ->whereNotIn('urls.url', $individualExcludedRoutes)
             ->where('user_agent_metadata.is_bot', false)
             ->where('status_code', 200)
+            ->whereNull('logged_requests.user_id')
             ->get();
 
         $now = now();
@@ -42,21 +57,54 @@ public function index(): View
         $totalVisitsPastThirtyDays = $visits->where('created_at', '>=', $now->copy()->subDays(30))->count();
         $totalVisitsAllTime = $visits->count();
 
-        $visitsPerDay = $visits->groupBy(fn ($visit) => Carbon::parse($visit->created_at)->format('Y-m-d'))
+        $periods = [
+            now()->format('Y-m-d') => 'Aujourd\'hui',
+            now()->subDay()->format('Y-m-d') => 'Hier',
+            now()->subDays(7)->format('Y-m-d') => 'Les 7 derniers jours',
+            now()->subDays(30)->format('Y-m-d') => 'Les 30 derniers jours',
+            now()->startOfMonth()->format('Y-m-d') => 'Ce mois-ci',
+            now()->subMonth()->startOfMonth()->format('Y-m-d') => 'Le mois dernier',
+        ];
+
+        if ($visits->isNotEmpty()) {
+            $periods[$visits->min('created_at')->format('Y-m-d')] = 'Depuis le début';
+        }
+
+        $startDate = $request->input('start_date', now()->subDays(30)->format('Y-m-d'));
+        $dateEnd = $request->input('end_date', now()->format('Y-m-d'));
+
+        $selectedPeriod = $startDate;
+
+        // Now, all the stats are calculated for the selected period
+        $visitsPerDay = $visits->where('created_at', '>=', $startDate)
+            ->where('created_at', '<=', $dateEnd)
+            ->groupBy(fn ($visit) => Carbon::parse($visit->created_at)->format('Y-m-d'))
             ->map(fn ($group) => ['date' => $group->first()->created_at->format('Y-m-d'), 'count' => $group->count()])
             ->values();
 
-        $visitsByCountry = $visits->groupBy('country_code')
+        $visitsByCountry = $visits->where('created_at', '>=', $startDate)
+            ->where('created_at', '<=', $dateEnd)
+            ->groupBy('country_code')
             ->map(fn ($group, $country) => ['country_code' => $country, 'count' => $group->count()])
             ->values();
 
-        $mostVisitedPages = $visits->groupBy('url')
+        $mostVisitedPages = $visits->where('created_at', '>=', $startDate)
+            ->where('created_at', '<=', $dateEnd)
+            ->groupBy('url')
             ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()])
             ->sortByDesc('count')
             ->values();
 
-        $mostVisitedPagesForPastTwentyFourHours = $visits->where('created_at', '>=', $now->copy()->subDay())
-            ->groupBy('url')
+        $bestsReferrers = $visits->where('created_at', '>=', $startDate)
+            ->where('created_at', '<=', $dateEnd)
+            ->groupBy('referer_url')
+            ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()])
+            ->sortByDesc('count')
+            ->values();
+
+        $bestOrigins = $visits->where('created_at', '>=', $startDate)
+            ->where('created_at', '<=', $dateEnd)
+            ->groupBy('origin_url')
             ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()])
             ->sortByDesc('count')
             ->values();
@@ -69,7 +117,10 @@ public function index(): View
             'visitsPerDay' => $visitsPerDay,
             'visitsByCountry' => $visitsByCountry,
             'mostVisitedPages' => $mostVisitedPages,
-            'mostVisitedPagesForPastTwentyFourHours' => $mostVisitedPagesForPastTwentyFourHours,
+            'bestsReferrers' => $bestsReferrers,
+            'bestOrigins' => $bestOrigins,
+            'periods' => $periods,
+            'selectedPeriod' => $selectedPeriod,
         ]);
     }
 }
diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php
index 8829ecf271ec3f55a2789d4bf3bec3ac54f256e0..557a1f8d4590a8bf0bec8a82fd27604fbb49a71f 100644
--- a/app/Http/Controllers/Admin/SettingsController.php
+++ b/app/Http/Controllers/Admin/SettingsController.php
@@ -4,6 +4,7 @@
 
 use App\Http\Controllers\Controller;
 use App\Mail\AdminInvitationMail;
+use App\Models\DbConfig;
 use App\Models\User;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
@@ -22,16 +23,30 @@ class SettingsController extends Controller
             'label' => 'Général',
             'icon' => 'circle-user',
         ],
-        [
-            'route' => 'admin.settings.users',
-            'label' => 'Utilisateurs',
-            'icon' => 'users',
-        ],
     ];
 
+    protected bool $isSuperAdmin;
+
+    public function __construct()
+    {
+        $this->isSuperAdmin = Auth::user()->email === config('app.protected_account_email');
+
+        if ($this->isSuperAdmin) {
+            $this->headerRoutes[] = [
+                'route' => 'admin.settings.users',
+                'label' => 'Utilisateurs',
+                'icon' => 'users',
+            ];
+            $this->headerRoutes[] = [
+                'route' => 'admin.settings.website-config',
+                'label' => 'Configuration du site',
+                'icon' => 'gear',
+            ];
+        }
+    }
+
     public function userListPage(): View
     {
-        $isSuperAdmin = auth()->user()->email === config('app.protected_account_email');
         $users = User::all();
 
         foreach ($users as $user) {
@@ -41,7 +56,7 @@ public function userListPage(): View
         return view('admin.settings.users', [
             'headerRoutes' => $this->headerRoutes,
             'users' => $users,
-            'isSuperAdmin' => $isSuperAdmin,
+            'isSuperAdmin' => $this->isSuperAdmin,
         ]);
     }
 
@@ -52,9 +67,7 @@ public function createUser(Request $request): RedirectResponse
             'email' => 'required|string|email|max:255|unique:users',
         ]);
 
-        $isSuperAdmin = Auth::user()->email === config('app.protected_account_email');
-
-        if (! $isSuperAdmin) {
+        if (! $this->isSuperAdmin) {
             return redirect()->route('admin.settings.users')
                 ->with('error', 'Vous n\'avez pas les droits pour effectuer cette action.');
         }
@@ -73,9 +86,7 @@ public function createUser(Request $request): RedirectResponse
 
     public function deleteUser(int $id): RedirectResponse
     {
-        $isSuperAdmin = auth()->user()->email === config('app.protected_account_email');
-
-        if (! $isSuperAdmin) {
+        if (! $this->isSuperAdmin) {
             return redirect()->route('admin.settings.users')
                 ->with('error', 'Vous n\'avez pas les droits pour effectuer cette action.');
         }
@@ -140,4 +151,39 @@ public function updateUserInfos(Request $request): RedirectResponse
 
         return redirect()->route('admin.settings.general')->with('success', 'Informations mises à jour.');
     }
+
+    public function websiteConfigPage(): RedirectResponse|View
+    {
+        if (! $this->isSuperAdmin) {
+            return redirect()->route('admin.settings.general')
+                ->with('error', 'Vous n\'avez pas les droits pour effectuer cette action.');
+        }
+
+        return view('admin.settings.website-config', [
+            'headerRoutes' => $this->headerRoutes,
+        ]);
+    }
+
+    public function updateWebsiteConfig(Request $request): RedirectResponse
+    {
+        $request->validate([
+            'maintenance_mode' => 'sometimes|nullable|string|in:true,false',
+            'website_name' => 'sometimes|nullable|string|max:255',
+        ]);
+
+        if (! $this->isSuperAdmin) {
+            return redirect()->route('admin.settings.general')
+                ->with('error', 'Vous n\'avez pas les droits pour effectuer cette action.');
+        }
+
+        if ($request->has('maintenance_mode')) {
+            DbConfig::set('maintenance_mode', $request->maintenance_mode);
+        }
+
+        if ($request->has('website_name')) {
+            DbConfig::set('website_name', $request->website_name);
+        }
+
+        return redirect()->route('admin.settings.website-config')->with('success', 'Configuration mise à jour.');
+    }
 }
diff --git a/app/Http/Controllers/Public/LegalMentionsController.php b/app/Http/Controllers/Public/LegalMentionsController.php
new file mode 100644
index 0000000000000000000000000000000000000000..4098697920ba33c78d7742a818a9902af53125af
--- /dev/null
+++ b/app/Http/Controllers/Public/LegalMentionsController.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Http\Controllers\Public;
+
+use App\Http\Controllers\Controller;
+use App\Models\LegalMention;
+
+class LegalMentionsController extends Controller
+{
+    public function __invoke()
+    {
+        $activeLegalMentionsText = LegalMention::where('active', true)->first();
+
+        if (! $activeLegalMentionsText) {
+            abort(404);
+        }
+        $activeLegalMentionsText = $activeLegalMentionsText->contentTransKey->getTranslation();
+
+        return view('public.legal-mentions', compact('activeLegalMentionsText'));
+    }
+}
diff --git a/app/Http/Controllers/Public/TermsController.php b/app/Http/Controllers/Public/TermsController.php
new file mode 100644
index 0000000000000000000000000000000000000000..a20736b21e40f1970a758be6b82ebd599c7956b0
--- /dev/null
+++ b/app/Http/Controllers/Public/TermsController.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Controllers\Public;
+
+use App\Http\Controllers\Controller;
+use App\Models\TermsSection;
+use Illuminate\View\View;
+
+class TermsController extends Controller
+{
+    public function __invoke(): View
+    {
+        $activeTermsText = TermsSection::where('active', true)->first();
+
+        if (! $activeTermsText) {
+            abort(404);
+        }
+        $activeTermsText = $activeTermsText->contentTranslationKey->getTranslation();
+
+        return view('public.terms', compact('activeTermsText'));
+    }
+}
diff --git a/app/Http/Middleware/CheckPrivateModeMiddleware.php b/app/Http/Middleware/CheckPrivateModeMiddleware.php
index 2e9ba639dc3fd4fac8951d165f007a3aa89a07e7..ca09e19f96a6e7fee067ba578508706656cd8b68 100644
--- a/app/Http/Middleware/CheckPrivateModeMiddleware.php
+++ b/app/Http/Middleware/CheckPrivateModeMiddleware.php
@@ -2,6 +2,7 @@
 
 namespace App\Http\Middleware;
 
+use App\Models\DbConfig;
 use Closure;
 use Illuminate\Http\Request;
 
@@ -9,9 +10,20 @@ class CheckPrivateModeMiddleware
 {
     public function handle(Request $request, Closure $next)
     {
-        if (config('app.private_mode') && ! $request->is('maintenance') && ! auth()->check()) {
-            return redirect()->route('maintenance');
-        } elseif (! config('app.private_mode') && $request->is('maintenance')) {
+        $privateModeEnabled = DbConfig::get('maintenance_mode', 'true');
+        $privateModeSecret = config('app.private_mode_secret');
+        $userSecretInput = $request->input('secret');
+        $secretIsUsable = ! empty($privateModeSecret) && $privateModeSecret === $userSecretInput;
+
+        if ($privateModeEnabled) {
+            if (! $secretIsUsable && ! auth()->check()) {
+                if (! $request->is('maintenance')) {
+                    return redirect()->route('maintenance');
+                }
+            }
+        }
+
+        if (! $privateModeEnabled && $request->is('maintenance') && ! auth()->check()) {
             return redirect()->route('index');
         }
 
diff --git a/app/Jobs/ProcessIpAddressesJob.php b/app/Jobs/ProcessIpAddressesJob.php
new file mode 100644
index 0000000000000000000000000000000000000000..d02f8cd33ecd23af984eb247bfdb10128ff18392
--- /dev/null
+++ b/app/Jobs/ProcessIpAddressesJob.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\IpAddressMetadata;
+use App\Services\IpAddressMetadataResolverService;
+use Exception;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Log;
+
+class ProcessIpAddressesJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public function __construct(private readonly Collection $ipAddresses) {}
+
+    public function handle(IpAddressMetadataResolverService $ipAddressMetadataResolver): void
+    {
+        try {
+            $metadataObjects = $ipAddressMetadataResolver->resolve($this->ipAddresses);
+        } catch (Exception $exception) {
+            report($exception);
+            $this->fail($exception);
+
+            return;
+        }
+
+        foreach ($metadataObjects as $metadata) {
+            if ($metadata['status'] === 'fail') {
+                Log::warning('Failed to resolve IP address metadata.', [
+                    'query' => $metadata['query'],
+                    'message' => $metadata['message'],
+                ]);
+
+                continue;
+            }
+
+            IpAddressMetadata::create([
+                'ip_address_id' => $this->ipAddresses->where('ip', $metadata['query'])->first()->id,
+                'country_code' => $metadata['countryCode'],
+                'lat' => $metadata['lat'],
+                'lon' => $metadata['lon'],
+            ]);
+        }
+    }
+}
diff --git a/app/Jobs/TranslateCreationJob.php b/app/Jobs/TranslateCreationJob.php
new file mode 100644
index 0000000000000000000000000000000000000000..152419f5cf4d53057709885073e673910ec8b018
--- /dev/null
+++ b/app/Jobs/TranslateCreationJob.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\Creation;
+use App\Models\Translation;
+use App\Services\AiProviderService;
+use Exception;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+
+class TranslateCreationJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public function __construct(private readonly Creation $creation) {}
+
+    public function handle(AiProviderService $aiProviderService): void
+    {
+        $frenchName = $this->creation->nameTranslationKey->getTranslation('fr');
+        $frenchShortDesc = $this->creation->shortDescriptionTranslationKey->getTranslation('fr');
+        $frenchDesc = $this->creation->descriptionTranslationKey->getTranslation('fr');
+
+        $shortDescriptionTranslationKeyId = $this->creation->shortDescriptionTranslationKey->id;
+        $descriptionTranslationKeyId = $this->creation->descriptionTranslationKey->id;
+
+        if (! empty($frenchName)) {
+            Translation::updateOrCreate(
+                ['translation_key_id' => $this->creation->name_translation_key_id, 'locale' => 'en'],
+                ['text' => $frenchName]
+            );
+            Cache::forget("translation_key_{$this->creation->name_translation_key_id}_en");
+        }
+
+        if (! empty($frenchShortDesc)) {
+            Translation::updateOrCreate(
+                ['translation_key_id' => $shortDescriptionTranslationKeyId, 'locale' => 'en'],
+                ['text' => $this->translate($frenchShortDesc, $aiProviderService)]
+            );
+            Cache::forget("translation_key_{$shortDescriptionTranslationKeyId}_en");
+        }
+
+        if (! empty($frenchDesc)) {
+            Translation::updateOrCreate(
+                ['translation_key_id' => $descriptionTranslationKeyId, 'locale' => 'en'],
+                ['text' => $this->translate($frenchDesc, $aiProviderService)]
+            );
+            Cache::forget("translation_key_{$descriptionTranslationKeyId}_en");
+        }
+    }
+
+    /**
+     * Translate the given text from French to English
+     *
+     * @param  string  $text  The text to translate
+     * @param  AiProviderService  $aiProviderService  The AI provider service
+     * @return string The translated text
+     */
+    private function translate(string $text, AiProviderService $aiProviderService): string
+    {
+        try {
+            $result = $aiProviderService->prompt(
+                'You are a helpful assistant that translates french markdown text in english and that outputs JSON in the format {message:string}. Markdown is supported.',
+                $text
+            );
+
+            return $result['message'] ?? '';
+        } catch (Exception $e) {
+            Log::error("Failed to translate text: {$text}", [
+                'exception' => $e->getMessage(),
+            ]);
+        }
+
+        return '';
+    }
+}
diff --git a/app/Models/DbConfig.php b/app/Models/DbConfig.php
new file mode 100644
index 0000000000000000000000000000000000000000..cddde72e647769d79782790b8869f4363605175e
--- /dev/null
+++ b/app/Models/DbConfig.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Cache;
+
+class DbConfig extends Model
+{
+    use HasFactory;
+
+    public $timestamps = false;
+
+    protected $fillable = [
+        'key',
+        'value',
+    ];
+
+    /**
+     * Get a configuration value by its key.
+     * Value types casts:
+     * - 'true' => true
+     * - 'false' => false
+     * - integers => int
+     * - floats => float
+     * - strings => string
+     *
+     * @param  string  $key  The key of the configuration value. Example: 'app.name'
+     * @param  float|bool|int|string|null  $default  The default value to return if the configuration does not exist.
+     * @return float|bool|int|string|null The value of the configuration.
+     */
+    public static function get(string $key, float|bool|int|string|null $default = null): float|bool|int|string|null
+    {
+        $config = Cache::rememberForever('config_'.$key, function () use ($key) {
+            return self::where('key', $key)->first();
+        });
+
+        if (empty($config) || ! $config->exists || $config->value === null) {
+            return $default;
+        }
+
+        // Cast to boolean if the value is a boolean
+        if ($config->value === 'true') {
+            return true;
+        } elseif ($config->value === 'false') {
+            return false;
+        }
+
+        // Cast to integer if the value is an integer
+        if (is_numeric($config->value)) {
+            if (is_float($config->value)) {
+                return (float) $config->value;
+            } else {
+                return (int) $config->value;
+            }
+        }
+
+        return $config->value;
+    }
+
+    /**
+     * Set a configuration value by its key.
+     *
+     * @param  string  $key  The key of the configuration value. Example: 'app.name'
+     * @param  string|int|float|bool  $value  The value of the configuration.
+     */
+    public static function set(string $key, float|bool|int|string|null $value): void
+    {
+        if (Cache::has('config_'.$key)) {
+            Cache::forget('config_'.$key);
+        }
+
+        $config = self::where('key', $key)->first();
+
+        if ($value === null) {
+            $config?->delete();
+
+            return;
+        }
+
+        if ($config === null) {
+            self::create([
+                'key' => $key,
+                'value' => $value,
+            ]);
+        } else {
+            $config->update([
+                'value' => $value,
+            ]);
+        }
+
+        Cache::rememberForever('config_'.$key, function () use ($key) {
+            return self::where('key', $key)->first();
+        });
+    }
+}
diff --git a/app/Models/IpAddressMetadata.php b/app/Models/IpAddressMetadata.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c0f61999c3b791d570c8b0ec353d43614b7c725
--- /dev/null
+++ b/app/Models/IpAddressMetadata.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+
+class IpAddressMetadata extends Model
+{
+    use HasFactory;
+
+    public $timestamps = false;
+
+    protected $fillable = [
+        'ip_address_id',
+        'country_code',
+        'lat',
+        'lon',
+    ];
+
+    const COUNTRY_CODES = ['AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ', 'BA', 'BB', 'BC', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BK', 'BL', 'BM', 'BN', 'BO', 'BP', 'BQ', 'BR', 'BS', 'BT', 'BU', 'BV', 'BW', 'BX', 'BY', 'BZ', 'CA', 'CB', 'CC', 'CD', 'CE', 'CF', 'CG', 'CH', 'CI', 'CJ', 'CK', 'CL', 'CM', 'CN', 'CO', 'CP', 'CQ', 'CR', 'CS', 'CT', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DA', 'DB', 'DC', 'DD', 'DE', 'DF', 'DG', 'DH', 'DI', 'DJ', 'DK', 'DL', 'DM', 'DN', 'DO', 'DP', 'DQ', 'DR', 'DS', 'DT', 'DU', 'DV', 'DW', 'DX', 'DY', 'DZ', 'EA', 'EB', 'EC', 'ED', 'EE', 'EF', 'EG', 'EH', 'EI', 'EJ', 'EK', 'EL', 'EM', 'EN', 'EO', 'EP', 'EQ', 'ER', 'ES', 'ET', 'EU', 'EV', 'EW', 'EX', 'EY', 'EZ', 'FA', 'FB', 'FC', 'FD', 'FE', 'FF', 'FG', 'FH', 'FI', 'FJ', 'FK', 'FL', 'FM', 'FN', 'FO', 'FP', 'FQ', 'FR', 'FS', 'FT', 'FU', 'FV', 'FW', 'FX', 'FY', 'FZ', 'GA', 'GB', 'GC', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GJ', 'GK', 'GL', 'GM', 'GN', 'GO', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GV', 'GW', 'GX', 'GY', 'GZ', 'HA', 'HB', 'HC', 'HD', 'HE', 'HF', 'HG', 'HH', 'HI', 'HJ', 'HK', 'HL', 'HM', 'HN', 'HO', 'HP', 'HQ', 'HR', 'HS', 'HT', 'HU', 'HV', 'HW', 'HX', 'HY', 'HZ', 'IA', 'IB', 'IC', 'ID', 'IE', 'IF', 'IG', 'IH', 'II', 'IJ', 'IK', 'IL', 'IM', 'IN', 'IO', 'IP', 'IQ', 'IR', 'IS', 'IT', 'IU', 'IV', 'IW', 'IX', 'IY', 'IZ', 'JA', 'JB', 'JC', 'JD', 'JE', 'JF', 'JG', 'JH', 'JI', 'JJ', 'JK', 'JL', 'JM', 'JN', 'JO', 'JP', 'JQ', 'JR', 'JS', 'JT', 'JU', 'JV', 'JW', 'JX', 'JY', 'JZ', 'KA', 'KB', 'KC', 'KD', 'KE', 'KF', 'KG', 'KH', 'KI', 'KJ', 'KK', 'KL', 'KM', 'KN', 'KO', 'KP', 'KQ', 'KR', 'KS', 'KT', 'KU', 'KV', 'KW', 'KX', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LD', 'LE', 'LF', 'LG', 'LH', 'LI', 'LJ', 'LK', 'LL', 'LM', 'LN', 'LO', 'LP', 'LQ', 'LR', 'LS', 'LT', 'LU', 'LV', 'LW', 'LX', 'LY', 'LZ', 'MA', 'MB', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MI', 'MJ', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NB', 'NC', 'ND', 'NE', 'NF', 'NG', 'NH', 'NI', 'NJ', 'NK', 'NL', 'NM', 'NN', 'NO', 'NP', 'NQ', 'NR', 'NS', 'NT', 'NU', 'NV', 'NW', 'NX', 'NY', 'NZ', 'OA', 'OB', 'OC', 'OD', 'OE', 'OF', 'OG', 'OH', 'OI', 'OJ', 'OK', 'OL', 'OM', 'ON', 'OO', 'OP', 'OQ', 'OR', 'OS', 'OT', 'OU', 'OV', 'OW', 'OX', 'OY', 'OZ', 'PA', 'PB', 'PC', 'PD', 'PE', 'PF', 'PG', 'PH', 'PI', 'PJ', 'PK', 'PL', 'PM', 'PN', 'PO', 'PP', 'PQ', 'PR', 'PS', 'PT', 'PU', 'PV', 'PW', 'PX', 'PY', 'PZ', 'QA', 'QB', 'QC', 'QD', 'QE', 'QF', 'QG', 'QH', 'QI', 'QJ', 'QK', 'QL', 'QM', 'QN', 'QO', 'QP', 'QQ', 'QR', 'QS', 'QT', 'QU', 'QV', 'QW', 'QX', 'QY', 'QZ', 'RA', 'RB', 'RC', 'RD', 'RE', 'RF', 'RG', 'RH', 'RI', 'RJ', 'RK', 'RL', 'RM', 'RN', 'RO', 'RP', 'RQ', 'RR', 'RS', 'RT', 'RU', 'RV', 'RW', 'RX', 'RY', 'RZ', 'SA', 'SB', 'SC', 'SD', 'SE', 'SF', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SP', 'SQ', 'SR', 'SS', 'ST', 'SU', 'SV', 'SW', 'SX', 'SY', 'SZ', 'TA', 'TB', 'TC', 'TD', 'TE', 'TF', 'TG', 'TH', 'TI', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TP', 'TQ', 'TR', 'TS', 'TT', 'TU', 'TV', 'TW', 'TX', 'TY', 'TZ', 'UA', 'UB', 'UC', 'UD', 'UE', 'UF', 'UG', 'UH', 'UI', 'UJ', 'UK', 'UL', 'UM', 'UN', 'UO', 'UP', 'UQ', 'UR', 'US', 'UT', 'UU', 'UV', 'UW', 'UX', 'UY', 'UZ', 'VA', 'VB', 'VC', 'VD', 'VE', 'VF', 'VG', 'VH', 'VI', 'VJ', 'VK', 'VL', 'VM', 'VN', 'VO', 'VP', 'VQ', 'VR', 'VS', 'VT', 'VU', 'VV', 'VW', 'VX', 'VY', 'VZ', 'WA', 'WB', 'WC', 'WD', 'WE', 'WF', 'WG', 'WH', 'WI', 'WJ', 'WK', 'WL', 'WM', 'WN', 'WO', 'WP', 'WQ', 'WR', 'WS', 'WT', 'WU', 'WV', 'WW', 'WX', 'WY', 'WZ', 'XA', 'XB', 'XC', 'XD', 'XE', 'XF', 'XG', 'XH', 'XI', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO', 'XP', 'XQ', 'XR', 'XS', 'XT', 'XU', 'XV', 'XW', 'XX', 'XY', 'XZ', 'YA', 'YB', 'YC', 'YD', 'YE', 'YF', 'YG', 'YH', 'YI', 'YJ', 'YK', 'YL', 'YM', 'YN', 'YO', 'YP', 'YQ', 'YR', 'YS', 'YT', 'YU', 'YV', 'YW', 'YX', 'YY', 'YZ', 'ZA', 'ZB', 'ZC', 'ZD', 'ZE', 'ZF', 'ZG', 'ZH', 'ZI', 'ZJ', 'ZK', 'ZL', 'ZM', 'ZN', 'ZO', 'ZP', 'ZQ', 'ZR', 'ZS', 'ZT', 'ZU', 'ZV', 'ZW', 'ZX', 'ZY', 'ZZ'];
+
+    public function ipAddress(): BelongsTo
+    {
+        return $this->belongsTo(IpAddress::class);
+    }
+}
diff --git a/app/Services/AiProviderService.php b/app/Services/AiProviderService.php
index 4b41c0262feaaba8b06882a50444b89fed2ccdf8..89800c6f85507dbdd688893115a0fc4e8b8d587a 100644
--- a/app/Services/AiProviderService.php
+++ b/app/Services/AiProviderService.php
@@ -16,6 +16,7 @@ class AiProviderService
      *
      * @param  string  $systemRole  The system role to send to the AI provider. E.g. "You are a helpful assistant."
      * @param  string  $prompt  The prompt to send to the AI provider
+     * @param  UploadedPicture  ...$pictures  The pictures to send to the AI provider
      * @return array The response from the AI provider.
      */
     public function promptWithPictures(string $systemRole, string $prompt, UploadedPicture ...$pictures): array
@@ -85,6 +86,13 @@ public function promptWithPictures(string $systemRole, string $prompt, UploadedP
         return $this->callApi(config('ai-provider.providers.'.$selectedProvider.'.url'), $requestBody);
     }
 
+    /**
+     * Prompt the AI provider with a text
+     *
+     * @param  string  $systemRole  The system role to send to the AI provider. E.g. "You are a helpful assistant."
+     * @param  string  $prompt  The prompt to send to the AI provider
+     * @return array The response from the AI provider.
+     */
     public function prompt(string $systemRole, string $prompt): array
     {
         $selectedProvider = config('ai-provider.selected-provider');
diff --git a/app/Services/IpAddressMetadataResolverService.php b/app/Services/IpAddressMetadataResolverService.php
new file mode 100644
index 0000000000000000000000000000000000000000..13f4eaec3cc317a5d6536bbe8ace6df74f42457a
--- /dev/null
+++ b/app/Services/IpAddressMetadataResolverService.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Services;
+
+use Exception;
+use Illuminate\Http\Client\ConnectionException;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+
+class IpAddressMetadataResolverService
+{
+    /**
+     * Resolve metadata for the given IP addresses.
+     * This method has a fail tolerance. The only exception that is thrown is when the API server returns a 422 status code.
+     * Server errors are logged and the method returns without throwing an exception.
+     *
+     * @param  Collection  $ipAddresses  The IP addresses to resolve metadata for. Instances of IpAddress.
+     * @return array{array{status: 'success'|'fail', message?: string, countryCode?: string, lat?: float, lon?: float, query: string}} The resolved metadata for each IP address.
+     *
+     * @throws ConnectionException
+     */
+    public static function resolve(Collection $ipAddresses): array
+    {
+        $url = config('ip-address-resolver.url').'?fields=status,message,countryCode,lat,lon,query';
+        $maxIpPerCall = config('ip-address-resolver.max_ip_addresses_per_call');
+        $maxCallsPerMinute = config('ip-address-resolver.call_limit_per_minute');
+        $currentCallsCount = Cache::get('ip-address-resolver.calls_count', 0);
+
+        if ($currentCallsCount >= $maxCallsPerMinute) {
+            Log::info('Max calls per minute reached. Skipping metadata resolution.');
+
+            return [];
+        }
+
+        Cache::increment('ip-address-resolver.calls_count', 1, 60);
+
+        if ($ipAddresses->count() > $maxIpPerCall) {
+            $ipAddresses = $ipAddresses->take($maxIpPerCall);
+        }
+
+        $response = Http::post($url, $ipAddresses->pluck('ip')->toArray());
+
+        if ($response->failed()) {
+            $returnedError = [
+                'status' => $response->status(),
+                'message' => $response->body(),
+            ];
+
+            if ($response->unprocessableContent()) {
+                $apiResponse = $response->json();
+                Log::error('The API rejected the request with a 422 unprocessable entity status code. ', $returnedError);
+                throw new Exception('The API rejected the request with a 422 unprocessable entity status code. '.$apiResponse['message']);
+            }
+            if ($response->serverError()) {
+                Log::info('The API server encountered an error while processing the request. Skipping metadata resolution.', $returnedError);
+
+                return [];
+            }
+            Log::error('The API server returned an unexpected status code. Skipping metadata resolution.', $returnedError);
+
+            return [];
+        }
+
+        return $response->json();
+    }
+}
diff --git a/composer.lock b/composer.lock
index fd9aca4cb1362286b38a0ca260169fbfbb84ce7d..31c03844cc31ebc1a50c1657014fe21bba8ec132 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1680,16 +1680,16 @@
         },
         {
             "name": "laravel/framework",
-            "version": "v11.44.0",
+            "version": "v11.43.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "e9a33da34815ac1ed46c7e4c477a775f4592f0a7"
+                "reference": "053f26afb699c845945e7380b407dd019a0a2c74"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/e9a33da34815ac1ed46c7e4c477a775f4592f0a7",
-                "reference": "e9a33da34815ac1ed46c7e4c477a775f4592f0a7",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/053f26afb699c845945e7380b407dd019a0a2c74",
+                "reference": "053f26afb699c845945e7380b407dd019a0a2c74",
                 "shasum": ""
             },
             "require": {
@@ -1891,7 +1891,7 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2025-02-24T13:08:54+00:00"
+            "time": "2025-02-19T16:06:03+00:00"
         },
         {
             "name": "laravel/horizon",
@@ -1975,21 +1975,21 @@
         },
         {
             "name": "laravel/octane",
-            "version": "v2.8.1",
+            "version": "v2.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/octane.git",
-                "reference": "951023e3c0ee8934d9f8338fd6495a0a969eefc4"
+                "reference": "1c5190cc5ad67eb4aadbf1816dcbfedc692851e7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/octane/zipball/951023e3c0ee8934d9f8338fd6495a0a969eefc4",
-                "reference": "951023e3c0ee8934d9f8338fd6495a0a969eefc4",
+                "url": "https://api.github.com/repos/laravel/octane/zipball/1c5190cc5ad67eb4aadbf1816dcbfedc692851e7",
+                "reference": "1c5190cc5ad67eb4aadbf1816dcbfedc692851e7",
                 "shasum": ""
             },
             "require": {
                 "laminas/laminas-diactoros": "^3.0",
-                "laravel/framework": "^10.10.1|^11.0|^12.0",
+                "laravel/framework": "^10.10.1|^11.0",
                 "laravel/prompts": "^0.1.24|^0.2.0|^0.3.0",
                 "laravel/serializable-closure": "^1.3|^2.0",
                 "nesbot/carbon": "^2.66.0|^3.0",
@@ -2010,9 +2010,9 @@
                 "livewire/livewire": "^2.12.3|^3.0",
                 "mockery/mockery": "^1.5.1",
                 "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0",
-                "orchestra/testbench": "^8.21|^9.0|^10.0",
+                "orchestra/testbench": "^8.21|^9.0",
                 "phpstan/phpstan": "^1.10.15",
-                "phpunit/phpunit": "^10.4|^11.5",
+                "phpunit/phpunit": "^10.4",
                 "spiral/roadrunner-cli": "^2.6.0",
                 "spiral/roadrunner-http": "^3.3.0"
             },
@@ -2061,7 +2061,7 @@
                 "issues": "https://github.com/laravel/octane/issues",
                 "source": "https://github.com/laravel/octane"
             },
-            "time": "2025-02-19T20:28:29+00:00"
+            "time": "2025-02-18T15:18:13+00:00"
         },
         {
             "name": "laravel/prompts",
@@ -2905,16 +2905,16 @@
         },
         {
             "name": "nesbot/carbon",
-            "version": "3.8.6",
+            "version": "3.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/CarbonPHP/carbon.git",
-                "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd"
+                "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
-                "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
+                "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/b1a53a27898639579a67de42e8ced5d5386aa9a4",
+                "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4",
                 "shasum": ""
             },
             "require": {
@@ -3007,7 +3007,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2025-02-20T17:33:38+00:00"
+            "time": "2025-02-11T16:28:45+00:00"
         },
         {
             "name": "nette/schema",
@@ -4871,37 +4871,37 @@
         },
         {
             "name": "spatie/laravel-markdown",
-            "version": "2.7.1",
+            "version": "2.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/spatie/laravel-markdown.git",
-                "reference": "353e7f9fae62826e26cbadef58a12ecf39685280"
+                "reference": "4ca6107d095c2f857c860462cfbe401e0c15fe0f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/spatie/laravel-markdown/zipball/353e7f9fae62826e26cbadef58a12ecf39685280",
-                "reference": "353e7f9fae62826e26cbadef58a12ecf39685280",
+                "url": "https://api.github.com/repos/spatie/laravel-markdown/zipball/4ca6107d095c2f857c860462cfbe401e0c15fe0f",
+                "reference": "4ca6107d095c2f857c860462cfbe401e0c15fe0f",
                 "shasum": ""
             },
             "require": {
-                "illuminate/cache": "^9.0|^10.0|^11.0|^12.0",
-                "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
-                "illuminate/support": "^9.0|^10.0|^11.0|^12.0",
-                "illuminate/view": "^9.0|^10.0|^11.0|^12.0",
+                "illuminate/cache": "^9.0|^10.0|^11.0",
+                "illuminate/contracts": "^9.0|^10.0|^11.0",
+                "illuminate/support": "^9.0|^10.0|^11.0",
+                "illuminate/view": "^9.0|^10.0|^11.0",
                 "league/commonmark": "^2.6.0",
                 "php": "^8.1",
                 "spatie/commonmark-shiki-highlighter": "^2.5",
                 "spatie/laravel-package-tools": "^1.4.3"
             },
             "require-dev": {
-                "brianium/paratest": "^6.2|^7.8",
-                "nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0",
-                "orchestra/testbench": "^6.15|^7.0|^8.0|^10.0",
-                "pestphp/pest": "^1.22|^2.0|^3.7",
-                "phpunit/phpunit": "^9.3|^11.5.3",
+                "brianium/paratest": "^6.2",
+                "nunomaduro/collision": "^5.3|^6.0",
+                "orchestra/testbench": "^6.15|^7.0|^8.0",
+                "pestphp/pest": "^1.22",
+                "phpunit/phpunit": "^9.3",
                 "spatie/laravel-ray": "^1.23",
-                "spatie/pest-plugin-snapshots": "^1.1|^2.2|^3.0",
-                "vimeo/psalm": "^4.8|^6.7"
+                "spatie/pest-plugin-snapshots": "^1.1",
+                "vimeo/psalm": "^4.8"
             },
             "type": "library",
             "extra": {
@@ -4935,7 +4935,7 @@
                 "spatie"
             ],
             "support": {
-                "source": "https://github.com/spatie/laravel-markdown/tree/2.7.1"
+                "source": "https://github.com/spatie/laravel-markdown/tree/2.7.0"
             },
             "funding": [
                 {
@@ -4943,7 +4943,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2025-02-21T13:43:18+00:00"
+            "time": "2024-12-10T15:11:24+00:00"
         },
         {
             "name": "spatie/laravel-package-tools",
@@ -5082,21 +5082,21 @@
         },
         {
             "name": "spatie/shiki-php",
-            "version": "2.3.2",
+            "version": "2.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/spatie/shiki-php.git",
-                "reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5"
+                "reference": "24b4dcc161f37144180edbef49557edb96c1dc2d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/spatie/shiki-php/zipball/a2e78a9ff8a1290b25d550be8fbf8285c13175c5",
-                "reference": "a2e78a9ff8a1290b25d550be8fbf8285c13175c5",
+                "url": "https://api.github.com/repos/spatie/shiki-php/zipball/24b4dcc161f37144180edbef49557edb96c1dc2d",
+                "reference": "24b4dcc161f37144180edbef49557edb96c1dc2d",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "php": "^8.0",
+                "php": "^7.4|^8.0",
                 "symfony/process": "^5.4|^6.4|^7.1"
             },
             "require-dev": {
@@ -5135,7 +5135,7 @@
                 "spatie"
             ],
             "support": {
-                "source": "https://github.com/spatie/shiki-php/tree/2.3.2"
+                "source": "https://github.com/spatie/shiki-php/tree/2.3.1"
             },
             "funding": [
                 {
@@ -5143,7 +5143,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2025-02-21T14:16:57+00:00"
+            "time": "2025-02-18T13:18:46+00:00"
         },
         {
             "name": "spatie/temporary-directory",
@@ -8766,16 +8766,16 @@
         },
         {
             "name": "phpmyadmin/sql-parser",
-            "version": "5.11.0",
+            "version": "5.10.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpmyadmin/sql-parser.git",
-                "reference": "07044bc8c13abd542756c3fd34dc66a5d6dee8e4"
+                "reference": "5346664973d10cf1abff20837fb1183f3c11a055"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/07044bc8c13abd542756c3fd34dc66a5d6dee8e4",
-                "reference": "07044bc8c13abd542756c3fd34dc66a5d6dee8e4",
+                "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/5346664973d10cf1abff20837fb1183f3c11a055",
+                "reference": "5346664973d10cf1abff20837fb1183f3c11a055",
                 "shasum": ""
             },
             "require": {
@@ -8790,11 +8790,9 @@
                 "phpbench/phpbench": "^1.1",
                 "phpmyadmin/coding-standard": "^3.0",
                 "phpmyadmin/motranslator": "^4.0 || ^5.0",
-                "phpstan/extension-installer": "^1.4",
-                "phpstan/phpstan": "^1.12",
-                "phpstan/phpstan-deprecation-rules": "^1.2",
-                "phpstan/phpstan-phpunit": "^1.4",
-                "phpstan/phpstan-strict-rules": "^1.6",
+                "phpstan/extension-installer": "^1.1",
+                "phpstan/phpstan": "^1.9.12",
+                "phpstan/phpstan-phpunit": "^1.3.3",
                 "phpunit/phpunit": "^8.5 || ^9.6",
                 "psalm/plugin-phpunit": "^0.16.1",
                 "vimeo/psalm": "^4.11",
@@ -8851,7 +8849,7 @@
                     "type": "other"
                 }
             ],
-            "time": "2025-02-22T20:00:59+00:00"
+            "time": "2025-01-19T04:14:02+00:00"
         },
         {
             "name": "phpstan/phpstan",
@@ -9236,16 +9234,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "11.5.9",
+            "version": "11.5.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "c91c830e7108a81e5845aeb6ba8fe3c1a4351c0b"
+                "reference": "c9bd61aab12f0fc5e82ecfe621ff518a1d1f1049"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c91c830e7108a81e5845aeb6ba8fe3c1a4351c0b",
-                "reference": "c91c830e7108a81e5845aeb6ba8fe3c1a4351c0b",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c9bd61aab12f0fc5e82ecfe621ff518a1d1f1049",
+                "reference": "c9bd61aab12f0fc5e82ecfe621ff518a1d1f1049",
                 "shasum": ""
             },
             "require": {
@@ -9255,7 +9253,7 @@
                 "ext-mbstring": "*",
                 "ext-xml": "*",
                 "ext-xmlwriter": "*",
-                "myclabs/deep-copy": "^1.13.0",
+                "myclabs/deep-copy": "^1.12.1",
                 "phar-io/manifest": "^2.0.4",
                 "phar-io/version": "^3.2.1",
                 "php": ">=8.2",
@@ -9317,7 +9315,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.9"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.8"
             },
             "funding": [
                 {
@@ -9333,7 +9331,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2025-02-21T06:08:50+00:00"
+            "time": "2025-02-18T06:26:59+00:00"
         },
         {
             "name": "sebastian/cli-parser",
@@ -10438,7 +10436,7 @@
     ],
     "aliases": [],
     "minimum-stability": "stable",
-    "stability-flags": [],
+    "stability-flags": {},
     "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
@@ -10447,6 +10445,6 @@
         "ext-pdo": "*",
         "ext-zip": "*"
     },
-    "platform-dev": [],
-    "plugin-api-version": "2.3.0"
+    "platform-dev": {},
+    "plugin-api-version": "2.6.0"
 }
diff --git a/config/app.php b/config/app.php
index b44d19f7b53e72e0c3756ca32a0d6d6540b17a67..5044bc0983f9c6575abfb3deba3697f93ed9988d 100644
--- a/config/app.php
+++ b/config/app.php
@@ -127,7 +127,7 @@
 
     'protected_account_email' => env('PROTECTED_ACCOUNT_EMAIL', ''),
 
-    'private_mode' => env('APP_PRIVATE_MODE', false),
+    // 'private_mode' => env('APP_PRIVATE_MODE', false),
 
     'imagick' => [
         'max_width' => env('IMAGICK_MAX_WIDTH', 16000), // 16k
@@ -136,4 +136,6 @@
     ],
 
     'cdn_disk' => env('CDN_FILESYSTEM_DISK'),
+
+    'private_mode_secret' => env('APP_PRIVATE_MODE_SECRET'),
 ];
diff --git a/config/ip-address-resolver.php b/config/ip-address-resolver.php
new file mode 100644
index 0000000000000000000000000000000000000000..7dc71a84fda14b4dd12a32ae5f08c7452edd2b95
--- /dev/null
+++ b/config/ip-address-resolver.php
@@ -0,0 +1,7 @@
+<?php
+
+return [
+    'url' => env('IP_ADDRESS_RESOLVER_URL', 'http://ip-api.com/batch'),
+    'call_limit_per_minute' => env('IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE', 15),
+    'max_ip_addresses_per_call' => env('IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL', 100),
+];
diff --git a/database/factories/DbConfigFactory.php b/database/factories/DbConfigFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..d1d7eae53fa035167c8d93a7e65764f08269fb1b
--- /dev/null
+++ b/database/factories/DbConfigFactory.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\DbConfig;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+class DbConfigFactory extends Factory
+{
+    protected $model = DbConfig::class;
+
+    public function definition(): array
+    {
+        return [
+            'key' => $this->faker->word(),
+            'value' => $this->faker->word(),
+        ];
+    }
+}
diff --git a/database/factories/IpAddressMetadataFactory.php b/database/factories/IpAddressMetadataFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..5581b8193b0a4a4f0f6a2edf952e61f5bcf10303
--- /dev/null
+++ b/database/factories/IpAddressMetadataFactory.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\IpAddressMetadata;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+
+class IpAddressMetadataFactory extends Factory
+{
+    protected $model = IpAddressMetadata::class;
+
+    public function definition(): array
+    {
+        return [
+            'country_code' => $this->faker->randomElement(IpAddressMetadata::COUNTRY_CODES),
+            'lat' => $this->faker->latitude(),
+            'lon' => $this->faker->longitude(),
+
+            'ip_address_id' => IpAddress::factory(),
+        ];
+    }
+}
diff --git a/database/migrations/2025_02_19_141925_create_ip_address_metadata_table.php b/database/migrations/2025_02_19_141925_create_ip_address_metadata_table.php
new file mode 100644
index 0000000000000000000000000000000000000000..387e34bb37c327ce10231111387850c065c3f420
--- /dev/null
+++ b/database/migrations/2025_02_19_141925_create_ip_address_metadata_table.php
@@ -0,0 +1,24 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('ip_address_metadata', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('ip_address_id')->constrained('ip_addresses');
+            $table->enum('country_code', ['AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ', 'BA', 'BB', 'BC', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BK', 'BL', 'BM', 'BN', 'BO', 'BP', 'BQ', 'BR', 'BS', 'BT', 'BU', 'BV', 'BW', 'BX', 'BY', 'BZ', 'CA', 'CB', 'CC', 'CD', 'CE', 'CF', 'CG', 'CH', 'CI', 'CJ', 'CK', 'CL', 'CM', 'CN', 'CO', 'CP', 'CQ', 'CR', 'CS', 'CT', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DA', 'DB', 'DC', 'DD', 'DE', 'DF', 'DG', 'DH', 'DI', 'DJ', 'DK', 'DL', 'DM', 'DN', 'DO', 'DP', 'DQ', 'DR', 'DS', 'DT', 'DU', 'DV', 'DW', 'DX', 'DY', 'DZ', 'EA', 'EB', 'EC', 'ED', 'EE', 'EF', 'EG', 'EH', 'EI', 'EJ', 'EK', 'EL', 'EM', 'EN', 'EO', 'EP', 'EQ', 'ER', 'ES', 'ET', 'EU', 'EV', 'EW', 'EX', 'EY', 'EZ', 'FA', 'FB', 'FC', 'FD', 'FE', 'FF', 'FG', 'FH', 'FI', 'FJ', 'FK', 'FL', 'FM', 'FN', 'FO', 'FP', 'FQ', 'FR', 'FS', 'FT', 'FU', 'FV', 'FW', 'FX', 'FY', 'FZ', 'GA', 'GB', 'GC', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GJ', 'GK', 'GL', 'GM', 'GN', 'GO', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GV', 'GW', 'GX', 'GY', 'GZ', 'HA', 'HB', 'HC', 'HD', 'HE', 'HF', 'HG', 'HH', 'HI', 'HJ', 'HK', 'HL', 'HM', 'HN', 'HO', 'HP', 'HQ', 'HR', 'HS', 'HT', 'HU', 'HV', 'HW', 'HX', 'HY', 'HZ', 'IA', 'IB', 'IC', 'ID', 'IE', 'IF', 'IG', 'IH', 'II', 'IJ', 'IK', 'IL', 'IM', 'IN', 'IO', 'IP', 'IQ', 'IR', 'IS', 'IT', 'IU', 'IV', 'IW', 'IX', 'IY', 'IZ', 'JA', 'JB', 'JC', 'JD', 'JE', 'JF', 'JG', 'JH', 'JI', 'JJ', 'JK', 'JL', 'JM', 'JN', 'JO', 'JP', 'JQ', 'JR', 'JS', 'JT', 'JU', 'JV', 'JW', 'JX', 'JY', 'JZ', 'KA', 'KB', 'KC', 'KD', 'KE', 'KF', 'KG', 'KH', 'KI', 'KJ', 'KK', 'KL', 'KM', 'KN', 'KO', 'KP', 'KQ', 'KR', 'KS', 'KT', 'KU', 'KV', 'KW', 'KX', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LD', 'LE', 'LF', 'LG', 'LH', 'LI', 'LJ', 'LK', 'LL', 'LM', 'LN', 'LO', 'LP', 'LQ', 'LR', 'LS', 'LT', 'LU', 'LV', 'LW', 'LX', 'LY', 'LZ', 'MA', 'MB', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MI', 'MJ', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NB', 'NC', 'ND', 'NE', 'NF', 'NG', 'NH', 'NI', 'NJ', 'NK', 'NL', 'NM', 'NN', 'NO', 'NP', 'NQ', 'NR', 'NS', 'NT', 'NU', 'NV', 'NW', 'NX', 'NY', 'NZ', 'OA', 'OB', 'OC', 'OD', 'OE', 'OF', 'OG', 'OH', 'OI', 'OJ', 'OK', 'OL', 'OM', 'ON', 'OO', 'OP', 'OQ', 'OR', 'OS', 'OT', 'OU', 'OV', 'OW', 'OX', 'OY', 'OZ', 'PA', 'PB', 'PC', 'PD', 'PE', 'PF', 'PG', 'PH', 'PI', 'PJ', 'PK', 'PL', 'PM', 'PN', 'PO', 'PP', 'PQ', 'PR', 'PS', 'PT', 'PU', 'PV', 'PW', 'PX', 'PY', 'PZ', 'QA', 'QB', 'QC', 'QD', 'QE', 'QF', 'QG', 'QH', 'QI', 'QJ', 'QK', 'QL', 'QM', 'QN', 'QO', 'QP', 'QQ', 'QR', 'QS', 'QT', 'QU', 'QV', 'QW', 'QX', 'QY', 'QZ', 'RA', 'RB', 'RC', 'RD', 'RE', 'RF', 'RG', 'RH', 'RI', 'RJ', 'RK', 'RL', 'RM', 'RN', 'RO', 'RP', 'RQ', 'RR', 'RS', 'RT', 'RU', 'RV', 'RW', 'RX', 'RY', 'RZ', 'SA', 'SB', 'SC', 'SD', 'SE', 'SF', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SP', 'SQ', 'SR', 'SS', 'ST', 'SU', 'SV', 'SW', 'SX', 'SY', 'SZ', 'TA', 'TB', 'TC', 'TD', 'TE', 'TF', 'TG', 'TH', 'TI', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TP', 'TQ', 'TR', 'TS', 'TT', 'TU', 'TV', 'TW', 'TX', 'TY', 'TZ', 'UA', 'UB', 'UC', 'UD', 'UE', 'UF', 'UG', 'UH', 'UI', 'UJ', 'UK', 'UL', 'UM', 'UN', 'UO', 'UP', 'UQ', 'UR', 'US', 'UT', 'UU', 'UV', 'UW', 'UX', 'UY', 'UZ', 'VA', 'VB', 'VC', 'VD', 'VE', 'VF', 'VG', 'VH', 'VI', 'VJ', 'VK', 'VL', 'VM', 'VN', 'VO', 'VP', 'VQ', 'VR', 'VS', 'VT', 'VU', 'VV', 'VW', 'VX', 'VY', 'VZ', 'WA', 'WB', 'WC', 'WD', 'WE', 'WF', 'WG', 'WH', 'WI', 'WJ', 'WK', 'WL', 'WM', 'WN', 'WO', 'WP', 'WQ', 'WR', 'WS', 'WT', 'WU', 'WV', 'WW', 'WX', 'WY', 'WZ', 'XA', 'XB', 'XC', 'XD', 'XE', 'XF', 'XG', 'XH', 'XI', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO', 'XP', 'XQ', 'XR', 'XS', 'XT', 'XU', 'XV', 'XW', 'XX', 'XY', 'XZ', 'YA', 'YB', 'YC', 'YD', 'YE', 'YF', 'YG', 'YH', 'YI', 'YJ', 'YK', 'YL', 'YM', 'YN', 'YO', 'YP', 'YQ', 'YR', 'YS', 'YT', 'YU', 'YV', 'YW', 'YX', 'YY', 'YZ', 'ZA', 'ZB', 'ZC', 'ZD', 'ZE', 'ZF', 'ZG', 'ZH', 'ZI', 'ZJ', 'ZK', 'ZL', 'ZM', 'ZN', 'ZO', 'ZP', 'ZQ', 'ZR', 'ZS', 'ZT', 'ZU', 'ZV', 'ZW', 'ZX', 'ZY', 'ZZ']);
+            $table->float('lat')->nullable();
+            $table->float('lon')->nullable();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('ip_address_metadata');
+    }
+};
diff --git a/database/migrations/2025_02_23_145044_create_db_configs_table.php b/database/migrations/2025_02_23_145044_create_db_configs_table.php
new file mode 100644
index 0000000000000000000000000000000000000000..09506350a1f0e6c5e8025e55bdc84ad74a00902f
--- /dev/null
+++ b/database/migrations/2025_02_23_145044_create_db_configs_table.php
@@ -0,0 +1,22 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('db_configs', function (Blueprint $table) {
+            $table->id();
+            $table->string('key')->unique();
+            $table->string('value');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('db_configs');
+    }
+};
diff --git a/lang/en/contact.php b/lang/en/contact.php
new file mode 100644
index 0000000000000000000000000000000000000000..87f2cd0a0374c6acae9a3c71e5e5fcb4dfb61cfe
--- /dev/null
+++ b/lang/en/contact.php
@@ -0,0 +1,6 @@
+<?php
+
+return [
+    'desc' => 'Do you have a project in mind or want to discuss your graphic design needs? Feel free to contact me! Whether it’s for a collaboration, a quote request, or just to exchange creative ideas, I’m here to help.',
+    'contact_me_on_one_of_my_social_media' => 'Contact me on one of my social media',
+];
diff --git a/lang/en/footer.php b/lang/en/footer.php
new file mode 100644
index 0000000000000000000000000000000000000000..8c46e83400e9f00b7abead2b34a3dd71c39d197e
--- /dev/null
+++ b/lang/en/footer.php
@@ -0,0 +1,14 @@
+<?php
+
+return [
+    'contact' => [
+        'title' => 'Excited to work with you!',
+        'desc' => 'Feel free to contact me if you need information, or if you just want to chat with me.',
+    ],
+    'links' => [
+        'title' => 'Stay in touch',
+    ],
+    'legals-infos' => [
+        'title' => 'Legal information',
+    ],
+];
diff --git a/lang/en/generic.php b/lang/en/generic.php
index 51de6fe9e666f20e6169dff894b319ce4b1416d9..a345a842c4e75dcc9747427837b12f5f10eb10f9 100644
--- a/lang/en/generic.php
+++ b/lang/en/generic.php
@@ -4,4 +4,6 @@
     'custom_needs' => 'A specific need?',
     'custom_needs_desc' => 'I am open to any proposal. Don’t hesitate to contact me!',
     'completed_in' => 'Completed in',
+    'legal_mentions' => 'Legal notices',
+    'terms' => 'Terms and conditions of sale',
 ];
diff --git a/lang/en/home.php b/lang/en/home.php
index 16cf7a51a14278df222b272557969e29d607dc6e..93efc9e769e02189d61ea81d70d4486e327e8d6d 100644
--- a/lang/en/home.php
+++ b/lang/en/home.php
@@ -17,4 +17,5 @@
         'waving_hand' => 'Waving hand emoji',
         'roxannas_photo' => 'Photo of Roxanna Valtre',
     ],
+    'meta_description' => 'Roxanna Valtre, Graphic Designer. Discover my services and my portfolio.',
 ];
diff --git a/lang/fr/contact.php b/lang/fr/contact.php
new file mode 100644
index 0000000000000000000000000000000000000000..67270fd422a5f39c002970a9ba9f6118848a3327
--- /dev/null
+++ b/lang/fr/contact.php
@@ -0,0 +1,6 @@
+<?php
+
+return [
+    'desc' => 'Vous avez un projet en tête ou souhaitez discuter de vos besoins en design graphique ? N\'hésitez pas à me contacter ! Que ce soit pour une collaboration, une demande de devis ou simplement pour échanger des idées créatives, je suis là pour vous aider.',
+    'contact_me_on_one_of_my_social_media' => 'Contactez moi sur l\'un de mes réseaux',
+];
diff --git a/lang/fr/footer.php b/lang/fr/footer.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e7acdedfc7c68d0ae44642d3a24acba707dda0b
--- /dev/null
+++ b/lang/fr/footer.php
@@ -0,0 +1,14 @@
+<?php
+
+return [
+    'contact' => [
+        'title' => 'Hâte de travailler avec vous !',
+        'desc' => 'N’hésitez pas à me contacter si vous avez besoin de renseignements, ou que vous souhaitez simplement discuter avec moi.',
+    ],
+    'links' => [
+        'title' => 'Restons en contact',
+    ],
+    'legals-infos' => [
+        'title' => 'Informations légales',
+    ],
+];
diff --git a/lang/fr/generic.php b/lang/fr/generic.php
index 4b27032c33a73fb3c47194d9a0b8197c2d786f5d..0c369358497101f7f0bfffd75983290fdc6b7610 100644
--- a/lang/fr/generic.php
+++ b/lang/fr/generic.php
@@ -4,4 +4,6 @@
     'custom_needs' => 'Un besoin particulier ?',
     'custom_needs_desc' => 'Je suis ouverte à toute proposition. N’hésitez pas à me contacter !',
     'completed_in' => 'Réalisé en',
+    'legal_mentions' => 'Mentions légales',
+    'terms' => 'Conditions générales de vente',
 ];
diff --git a/lang/fr/home.php b/lang/fr/home.php
index c873aff786363813b37944ba5b8b5a907b465aef..b24c61701f0f400b4156847f875a2809446a9418 100644
--- a/lang/fr/home.php
+++ b/lang/fr/home.php
@@ -17,4 +17,5 @@
         'waving_hand' => 'Emoji de main qui salue',
         'roxannas_photo' => 'Photo de Roxana Valtre',
     ],
+    'meta_description' => 'Roxanna Valtre, Designer Graphique. Découvrez mes services et mon portfolio.',
 ];
diff --git a/resources/js/pages/admin/home.js b/resources/js/pages/admin/home.js
index f6c7de90dd8ad0f688bcb885d1d3121f481aa37b..b431283491da6011be352ee4a65d5003cdadb9da 100644
--- a/resources/js/pages/admin/home.js
+++ b/resources/js/pages/admin/home.js
@@ -7,4 +7,12 @@ const app = createApp();
 app.component('visits-per-day-chart', VisitsPerDayChart);
 app.component('visits-by-country-chart', VisitsByCountryChart);
 
-app.mount('#admin-home')
\ No newline at end of file
+app.mount('#admin-home')
+
+const statsSelect = document.getElementById('statsSelect');
+statsSelect.addEventListener('change', () => {
+    let startDate = statsSelect.value;
+    let endDate = new Date().toISOString().split('T')[0];
+
+    window.location.href = `?start_date=${startDate}&end_date=${endDate}`;
+});
\ No newline at end of file
diff --git a/resources/scss/public.scss b/resources/scss/public.scss
index 2d88a1389a6b99742d97199479c04e37fe0dbbda..6a7d3c8e7010d4a3502bf9c1c9fee0a8bd89d6db 100644
--- a/resources/scss/public.scss
+++ b/resources/scss/public.scss
@@ -3,6 +3,10 @@
 @tailwind utilities;
 @import "fonts";
 
+h1, h2, h3, h4, h5, h6 {
+    font-family: "Sniglet", serif;
+}
+
 .markdown {
     * {
         @apply mb-4;
diff --git a/resources/views/admin/creations/index.blade.php b/resources/views/admin/creations/index.blade.php
index e13f5b4f9fcb48b70d6605515bbc45addf9ca8a3..c5e81d17f85c53b0cce028eff3327f3618c4f197 100644
--- a/resources/views/admin/creations/index.blade.php
+++ b/resources/views/admin/creations/index.blade.php
@@ -28,6 +28,11 @@
                                  size="sm" tag="a" variant="danger">
                         Supprimer
                     </x-bs.button>
+                    <x-bs.button size="sm" variant="secondary" id="translate-{{ $creation->id }}"
+                                 onclick="translateWithAi({{ $creation->id }})">
+                        {{ \Spatie\Emoji\Emoji::flagsForFlagUnitedKingdom() }}
+                        Traduire
+                    </x-bs.button>
                 </x-slot:footer>
             </x-bs.card>
         @endforeach
@@ -38,3 +43,24 @@
         @endif
     </div>
 @endsection
+
+@pushonce('scripts')
+    <script type="text/javascript">
+        function translateWithAi(creationId) {
+            const button = document.querySelector(`#translate-${creationId}`);
+            button.disabled = true;
+            fetch(`/admin/creations/${creationId}/translate-with-ai`)
+                .then(response => response.json())
+                .then(data => {
+                    if (data.success) {
+                        button.classList.remove('btn-secondary');
+                        button.classList.add('btn-success');
+                    } else {
+                        button.disabled = false;
+                        button.classList.remove('btn-secondary');
+                        button.classList.add('btn-warning');
+                    }
+                });
+        }
+    </script>
+@endpushonce
diff --git a/resources/views/admin/home.blade.php b/resources/views/admin/home.blade.php
index 98401498253489b1e32efa808c516f97f767c48e..7ab23f545144b56ba4a8f99f5f316d60e7d80428 100644
--- a/resources/views/admin/home.blade.php
+++ b/resources/views/admin/home.blade.php
@@ -2,9 +2,17 @@
 
 @section('content')
     <div class="container" id="admin-home">
+        @if(\App\Models\DbConfig::get('maintenance_mode', 'true'))
+            <x-bs.alert type="warning" :dismissible="false" icon="info" title="Information" style="max-width: none;">
+                Le site est masqué au public car le mode maintenance est activé.<br>
+                <a href="{{ route('admin.settings.update-website-config') }}" class="alert-link">
+                    Rendez-vous sur la page de configuration du site pour le désactiver.
+                </a>
+            </x-bs.alert>
+        @endif
         <h3 class="mb-4">Statistiques des Visites uniques</h3>
 
-        <div class="grid g-4 mb-4">
+        <div class="grid mb-4">
             <!-- Total des visites -->
             <div class="g-col-12 g-col-md-6 g-col-lg-3">
                 <div class="card text-center">
@@ -40,7 +48,14 @@
             </div>
         </div>
 
-        <div class="row g-4 mb-4">
+        <div class="grid mb-4">
+            <div class="g-col-12 g-col-md-6 g-col-lg-3">
+                <x-bs.select id="statsSelect" label="Période de statistiques" name="period"
+                             :options="$periods" :selected="$selectedPeriod"/>
+            </div>
+        </div>
+
+        <div class="row mb-4">
             <!-- Visites par jour -->
             <div class="g-col-md-6 mb-4">
                 <div class="card">
@@ -72,7 +87,7 @@
             <div class="g-col-12 mb-4">
                 <div class="card">
                     <div class="card-body">
-                        <h5 class="card-title">Pages les plus visitées (24H)</h5>
+                        <h5 class="card-title">Pages les plus visitées</h5>
                         <div class="table-responsive">
                             <table class="table table-striped align-middle">
                                 <thead>
@@ -83,7 +98,7 @@
                                 </tr>
                                 </thead>
                                 <tbody>
-                                @foreach ($mostVisitedPagesForPastTwentyFourHours as $index => $page)
+                                @foreach ($mostVisitedPages as $index => $page)
                                     <tr>
                                         <th scope="row">{{ $index + 1 }}</th>
                                         <td>{{ $page['url'] }}</td>
@@ -97,25 +112,57 @@
                 </div>
             </div>
 
+            <!-- Meilleurs referers -->
+            <div class="g-col-12 mb-4">
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">
+                            Referers (sites référents) les plus fréquents
+                        </h5>
+                        <div class="table-responsive">
+                            <table class="table table-striped align-middle">
+                                <thead>
+                                <tr>
+                                    <th scope="col">#</th>
+                                    <th scope="col">Referer</th>
+                                    <th scope="col">Nombre de visites</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                @foreach ($bestsReferrers as $index => $referer)
+                                    <tr>
+                                        <th scope="row">{{ $index + 1 }}</th>
+                                        <td>{{ $referer['url'] }}</td>
+                                        <td>{{ $referer['count'] }}</td>
+                                    </tr>
+                                @endforeach
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Meilleures origines -->
             <div class="g-col-12">
                 <div class="card">
                     <div class="card-body">
-                        <h5 class="card-title">Pages les plus visitées</h5>
+                        <h5 class="card-title">Origines les plus fréquentes</h5>
                         <div class="table-responsive">
                             <table class="table table-striped align-middle">
                                 <thead>
                                 <tr>
                                     <th scope="col">#</th>
-                                    <th scope="col">Page</th>
+                                    <th scope="col">Origine</th>
                                     <th scope="col">Nombre de visites</th>
                                 </tr>
                                 </thead>
                                 <tbody>
-                                @foreach ($mostVisitedPages as $index => $page)
+                                @foreach ($bestOrigins as $index => $origin)
                                     <tr>
                                         <th scope="row">{{ $index + 1 }}</th>
-                                        <td>{{ $page['url'] }}</td>
-                                        <td>{{ $page['count'] }}</td>
+                                        <td>{{ $origin['url'] }}</td>
+                                        <td>{{ $origin['count'] }}</td>
                                     </tr>
                                 @endforeach
                                 </tbody>
diff --git a/resources/views/admin/settings/general.blade.php b/resources/views/admin/settings/general.blade.php
index ef891f725d4090d82f6aad10f774fe5fc8668061..6ea34cd993b44b73a28a9be5dd5f29bb6afbe364 100644
--- a/resources/views/admin/settings/general.blade.php
+++ b/resources/views/admin/settings/general.blade.php
@@ -3,10 +3,32 @@
 @section('content')
     <div class="grid">
         <div class="g-col-12 g-col-md-6">
+            @if (!empty($errors) && $errors->any())
+                <x-bs.alert type="danger" class="mt-3">
+                    <ul>
+                        @foreach ($errors->all() as $error)
+                            <li>{{ $error }}</li>
+                        @endforeach
+                    </ul>
+                </x-bs.alert>
+            @endif
+
+            @if (session('success'))
+                <x-bs.alert type="success" class="mt-3">
+                    {{ session('success') }}
+                </x-bs.alert>
+            @endif
+
+            @if (session('error'))
+                <x-bs.alert type="danger" class="mt-3">
+                    {{ session('error') }}
+                </x-bs.alert>
+            @endif
+
             <h5>Information</h5>
             <p>Ne remplissez que les champs que vous souhaitez mettre à jour.</p>
             <form action="{{ route('admin.settings.update-user-infos') }}"
-                    method="post">
+                  method="post">
                 @csrf
                 @method('PUT')
 
@@ -48,22 +70,6 @@ class="mb-3"
 
                 <x-bs.button type="submit">Enregistrer</x-bs.button>
             </form>
-
-            @if (!empty($errors) && $errors->any())
-                <x-bs.alert type="danger" class="mt-3">
-                    <ul>
-                        @foreach ($errors->all() as $error)
-                            <li>{{ $error }}</li>
-                        @endforeach
-                    </ul>
-                </x-bs.alert>
-            @endif
-
-            @if (session('success'))
-                <x-bs.alert type="success" class="mt-3">
-                    {{ session('success') }}
-                </x-bs.alert>
-            @endif
         </div>
     </div>
 @endsection
diff --git a/resources/views/admin/settings/users.blade.php b/resources/views/admin/settings/users.blade.php
index 57713da07d490c06ac3a5e2197c6641d2ebdc6de..a3bc900930e1c70365180da2a6845f843087cc05 100644
--- a/resources/views/admin/settings/users.blade.php
+++ b/resources/views/admin/settings/users.blade.php
@@ -7,7 +7,8 @@
             par
             email.</p>
         <p><span class="fw-bold text-danger">Attention !</span> Pour des raisons de simplicité, ces personnes auront les
-            mêmes droits que vous (sauf toutes les actions liées à cette page, y compris son accès).
+            mêmes droits que vous (sauf toutes les actions liées à cette page et à la configuration du site, y compris
+            leur accès).
             <strong>Soyez bien certains d'avoir confiance en elles !</strong></p>
 
         <x-bs.button data-bs-toggle="modal" data-bs-target="#addUserModal" size="sm" variant="primary">
diff --git a/resources/views/admin/settings/website-config.blade.php b/resources/views/admin/settings/website-config.blade.php
new file mode 100644
index 0000000000000000000000000000000000000000..4e15c67a69f6f055c1c0a5c033fe304a6113a167
--- /dev/null
+++ b/resources/views/admin/settings/website-config.blade.php
@@ -0,0 +1,48 @@
+@extends('layouts.admin', ['title' => 'Configuration', 'headerRoutes' => $headerRoutes])
+
+@section('content')
+    <div class="grid">
+        <div class="g-col-12 g-col-md-6">
+            @if (!empty($errors) && $errors->any())
+                <x-bs.alert type="danger" class="mt-3">
+                    <ul>
+                        @foreach ($errors->all() as $error)
+                            <li>{{ $error }}</li>
+                        @endforeach
+                    </ul>
+                </x-bs.alert>
+            @endif
+
+            @if (session('success'))
+                <x-bs.alert type="success" class="mt-3">
+                    {{ session('success') }}
+                </x-bs.alert>
+            @endif
+
+            <h5>Configuration</h5>
+            <p>Modifiez les paramètres du site internet.</p>
+            <form action="{{ route('admin.settings.update-website-config') }}"
+                  method="post">
+                @csrf
+                @method('PUT')
+
+                <x-bs.select label="Statut du mode maintenance"
+                             name="maintenance_mode"
+                             class="mb-3"
+                             :options="['false' => 'Désactivé', 'true' => 'Activé']"
+                             :selected="old('maintenance_mode', \App\Models\DbConfig::get('maintenance_mode', 'true'))"
+                             data-form-type="other"
+                />
+
+                <x-bs.input label="Nom du site internet"
+                            name="website_name"
+                            class="mb-3"
+                            :value="old('website_name', \App\Models\DbConfig::get('website_name', 'Rann Graphic Design'))"
+                            data-form-type="other"
+                />
+
+                <x-bs.button type="submit">Enregistrer</x-bs.button>
+            </form>
+        </div>
+    </div>
+@endsection
diff --git a/resources/views/components/admin/sidebar.blade.php b/resources/views/components/admin/sidebar.blade.php
index 8358c73a3c50adced06c560f5e28f5d12c40e1dc..383a4b3d856cbeb330842c8fc4114a68f805359e 100644
--- a/resources/views/components/admin/sidebar.blade.php
+++ b/resources/views/components/admin/sidebar.blade.php
@@ -8,7 +8,7 @@
         <a class="website-name" href="{{ route('admin.home') }}">
             <img class="logo" src="{{ asset('favicon.svg') }}" alt="Website Logo"/>
             <span>
-                {{ config('app.name') }}
+                {{ \App\Models\DbConfig::get('website_name', "Rann Graphic Design") }}
             </span>
         </a>
     </div>
diff --git a/resources/views/components/public/footer.blade.php b/resources/views/components/public/footer.blade.php
index 05142fdf514118c6824e2c1982921cb4f779788b..de488c30d597cdcf66ee759c5d8e1a0adce70b94 100644
--- a/resources/views/components/public/footer.blade.php
+++ b/resources/views/components/public/footer.blade.php
@@ -8,20 +8,20 @@
                         <x-public.rann-logo height="4rem" role="image"/>
                     </div>
                     <div class="self-stretch flex-col justify-start items-start gap-2 flex">
-                        <div class="self-stretch text-2xl font-bold">Hâte de travailler avec vous !</div>
-                        <div class="self-stretch text-muted">N’hésitez pas à me contacter si vous avez besoin de
-                            renseignements, ou que vous souhaitez simplement discuter avec moi.
+                        <div class="self-stretch text-2xl font-bold">{{ __('footer.contact.title') }}</div>
+                        <div class="self-stretch text-muted">
+                            {{ __('footer.contact.desc') }}
                         </div>
                     </div>
-                    <x-public.button size="medium" tag="a" href="#">
-                        <div class="text-xl font-bold">Me contacter</div>
+                    <x-public.button size="medium" tag="a" href="{{ route('contact') }}">
+                        <div class="text-xl font-bold">{{ __('navbar.contact_me') }}</div>
                     </x-public.button>
                 </div>
             </div>
 
             <div class="w-full lg:w-auto items-start flex flex-col lg:flex-row gap-8 lg:gap-24">
                 <div class="w-full lg:w-56 flex-col gap-8 flex">
-                    <div class="self-stretch text-2xl font-bold">Restons en contact</div>
+                    <div class="self-stretch text-2xl font-bold">{{ __('footer.links.title') }}</div>
                     <ul class="flex flex-col gap-2">
                         @foreach(\App\Models\SocialMediaLink::all() as $socialMediaLink)
                             <li>
@@ -33,16 +33,15 @@
                     </ul>
                 </div>
                 <div class="w-full lg:w-56 flex-col gap-8 flex">
-                    <div class="self-stretch text-2xl font-bold">Informations légales</div>
+                    <div class="self-stretch text-2xl font-bold">{{ __('footer.legals-infos.title') }}</div>
                     <ul class="flex flex-col gap-2">
                         <li>
-                            <a href="#" class="text-muted hover:text-primary">Mentions légales</a>
+                            <a href="{{ route('legal-mentions') }}"
+                               class="text-muted hover:text-primary">{{ __('generic.legal_mentions') }}</a>
                         </li>
                         <li>
-                            <a href="#" class="text-muted hover:text-primary">Conditions générales de vente</a>
-                        </li>
-                        <li>
-                            <a href="#" class="text-muted hover:text-primary">Politique de confidentialité</a>
+                            <a href="{{ route('terms') }}"
+                               class="text-muted hover:text-primary">{{ __('generic.terms') }}</a>
                         </li>
                     </ul>
                 </div>
diff --git a/resources/views/components/public/generic-page-header.blade.php b/resources/views/components/public/generic-page-header.blade.php
index 7883746c0253491d0b38ca139d379b74f96a19ac..defaac73b1c21652eacf74dd4be847ddcc368361 100644
--- a/resources/views/components/public/generic-page-header.blade.php
+++ b/resources/views/components/public/generic-page-header.blade.php
@@ -1,6 +1,6 @@
 @props([
     'title' => 'Title',
-    'description' => 'Description',
+    'description' => '',
 ])
 
 <div {{ $attributes->class(['relative flex flex-col max-w-2xl gap-3']) }}>
diff --git a/resources/views/components/public/navbar.blade.php b/resources/views/components/public/navbar.blade.php
index 0c0612b1bfd4171d88c7cc3d51e6daad738b95b4..f4d69177c983f15c502f55f274dc573c730ff346 100644
--- a/resources/views/components/public/navbar.blade.php
+++ b/resources/views/components/public/navbar.blade.php
@@ -17,7 +17,7 @@
                 <a href="{{ route('prestations') }}" class="text-xl font-bold">{{ __('navbar.services') }}</a>
                 <a href="{{ route('portfolio') }}" class="text-xl font-bold">{{ __('navbar.portfolio') }}</a>
                 <a href="{{ route('evenements') }}" class="text-xl font-bold">{{ __('navbar.events') }}</a>
-                <a href="#" class="text-xl font-bold">{{ __('navbar.contact_me') }}</a>
+                <a href="{{ route('contact') }}" class="text-xl font-bold">{{ __('navbar.contact_me') }}</a>
                 <x-public.button tag="a" href="https://dalnarabyrann.sumupstore.com/">
                     {{ __('navbar.shop') }}
                 </x-public.button>
@@ -41,7 +41,7 @@ class="fixed z-50 inset-0 bg-white bg-opacity-90 flex-col items-center justify-c
         <a href="{{ route('prestations') }}" class="text-2xl font-bold">{{ __('navbar.services') }}</a>
         <a href="{{ route('portfolio') }}" class="text-2xl font-bold">{{ __('navbar.portfolio') }}</a>
         <a href="{{ route('evenements') }}" class="text-2xl font-bold">{{ __('navbar.events') }}</a>
-        <a href="#" class="text-2xl font-bold">{{ __('navbar.contact_me') }}</a>
+        <a href="{{ route('contact') }}" class="text-2xl font-bold">{{ __('navbar.contact_me') }}</a>
         <x-public.button tag="a" href="https://dalnarabyrann.sumupstore.com/" class="text-xl">
             {{ __('navbar.shop') }}
         </x-public.button>
diff --git a/resources/views/layouts/public.blade.php b/resources/views/layouts/public.blade.php
index f2a8ee119fe0b91ff7bf298db03819fd2e8c710d..51d9b8ef932156641ee214e8fb59941ef0e3a5c5 100644
--- a/resources/views/layouts/public.blade.php
+++ b/resources/views/layouts/public.blade.php
@@ -1,12 +1,34 @@
+@php
+    $websiteName = \App\Models\DbConfig::get('website_name', "Rann Graphic Design")
+@endphp
+
 <!DOCTYPE html>
 <html lang="{{ config('app.locale') }}">
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{{ !empty($title) ? $title . ' - Rann Graphic Design' : 'Rann Graphic Design' }}</title>
+    <title>{{ !empty($title) ? $title . ' - ' . $websiteName : $websiteName }}</title>
     <link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}"/>
+
+    <meta property="og:locale" content="{{ config('app.locale') }}">
+    <meta property="og:locale:alternate" content="{{ config('app.fallback_locale') }}">
+
+    <meta property="og:title" content="{{ !empty($title) ? $title : $websiteName }}">
+    <meta property="og:type" content="website">
+    <meta property="og:url" content="{{ url()->current() }}">
+    @if(!empty($description))
+        <meta name="description" content="{{ $description }}">
+        <meta property="og:description" content="{{ $description }}">
+    @endif
+    @if(!empty($image) && is_a($image, \App\Models\UploadedPicture::class))
+        <meta property="og:image" content="{{ $image->getFullsizeUrl() }}">
+        <meta property="og:image:width" content="{{ $image->width }}">
+        <meta property="og:image:height" content="{{ $image->height }}">
+    @endif
+
     <link rel="preconnect" href="https://fonts.googleapis.com">
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Sniglet:wght@400;800&display=swap" rel="stylesheet">
     @vite(['resources/scss/public.scss', 'resources/js/app.js'])
 </head>
 <body>
diff --git a/resources/views/public/contact.blade.php b/resources/views/public/contact.blade.php
new file mode 100644
index 0000000000000000000000000000000000000000..74e56ac3a417a67230e323a6e33024e1ae361cfe
--- /dev/null
+++ b/resources/views/public/contact.blade.php
@@ -0,0 +1,30 @@
+@extends('layouts.public', ['title' => __('navbar.contact_me'), 'description' => __('contact.desc')])
+
+@section('content')
+    <x-public.navbar class="container mx-auto px-4"/>
+    <div class="container mx-auto px-4 py-24 flex flex-col gap-16">
+        <x-public.generic-page-header title="{{ __('navbar.contact_me') }}" :description="__('contact.desc')"/>
+
+        <div>
+            <div class="text-center text-2xl">
+                {{ __('contact.contact_me_on_one_of_my_social_media') }}
+            </div>
+
+            <div class="flex flex-wrap align-center justify-center gap-4 mt-4">
+                @foreach(\App\Models\SocialMediaLink::all() as $socialMediaLink)
+                    <x-public.button tag="a" :href="$socialMediaLink->url" class="flex items-center gap-2"
+                                     size="medium">
+                        <div>
+                            {{ $socialMediaLink->name }}
+                        </div>
+                        <div>
+                            <x-font-awesome :icon="$socialMediaLink->icon_name" type="brands" class="w-6 h-6"/>
+                        </div>
+                    </x-public.button>
+                @endforeach
+            </div>
+        </div>
+    </div>
+
+    <x-public.footer/>
+@endsection
\ No newline at end of file
diff --git a/resources/views/public/index.blade.php b/resources/views/public/index.blade.php
index 72ec9b60bf89d33f28586896b118e7ec2be50ec5..ed2c352e43576e2d6270065ef23cbba0c12874b3 100644
--- a/resources/views/public/index.blade.php
+++ b/resources/views/public/index.blade.php
@@ -1,4 +1,4 @@
-@extends('layouts.public')
+@extends('layouts.public', ['description' => __('home.meta_description')])
 
 @section('content')
     <section class="HeroSection position-relative w-100">
@@ -21,11 +21,12 @@ class="absolute hidden 2xl:block left-[-96px] top-[-26px] origin-top-left rotate
                                     </span>
                                     <x-public.arrow class="hidden xl:block w-28 h-9"/>
                                 </span>
-                                <span class="text-5xl xl:text-6xl font-bold">
+                                <span class="text-5xl xl:text-6xl">
                                     {{ __('home.hero.second_row') }}
                                 </span>
                             </h1>
-                            <x-public.button size="large" tag="a" href="#" class="flex items-center gap-8">
+                            <x-public.button size="large" tag="a" href="{{ route('contact') }}"
+                                             class="flex items-center gap-8">
                                 <img class="w-10 h-10" src="{{Vite::asset("resources/images/public/waving-hand.avif")}}"
                                      alt="{{ __('home.img_alt.waving_hand') }}" loading="eager"/>
                                 <div class="flex flex-col justify-center items-start">
@@ -71,18 +72,8 @@ class="Creations relative hidden xl:grid grow w-full aspect-[885/622]"/>
         <div class="w-full h-24 bg-gradient-to-b from-neutral-200 to-light"></div>
         <div class="container mx-auto px-4 flex flex-col justify-start items-center gap-2.5">
             <div class="flex flex-col justify-start items-start w-full gap-16">
-                <div class="flex flex-col justify-start items-start max-w-6xl">
-                    <div class="text-2xl font-bold">{{ __('home.about_me') }}</div>
-                    <h2 class="text-5xl xl:text-6xl font-normal">
-                        @if(!$aboutMeSection->isEmpty())
-                            {{ $aboutMeSection->first()->attractDescTransKey->getTranslation() }}
-                        @else
-                            Lorem ipsum dolor sit amet,
-                        @endif
-                    </h2>
-                </div>
-                <div class="flex flex-col xl:flex-row justify-start items-center w-full gap-4 xl:gap-0">
-                    <div class="flex shrink-0 basis-1/2 justify-center items-center p-4 xl:p-0 gap-2.5">
+                <div class="flex flex-col xl:flex-row justify-start w-full gap-8 xl:gap-16">
+                    <div class="flex shrink-0 justify-center items-center p-4 xl:p-0 gap-2.5">
                         <div class="overflow-hidden w-full xl:w-[25rem] aspect-square rounded-2xl shadow-[-.5rem_.5rem_0px_0px_rgba(0,0,0,1.00)] shadow-pink-normal">
                             @if(!$aboutMeSection->isEmpty())
                                 <img class="w-full h-full object-cover"
@@ -95,8 +86,17 @@ class="Creations relative hidden xl:grid grow w-full aspect-[885/622]"/>
                             @endif
                         </div>
                     </div>
-                    <div class="flex flex-col shrink-0 basis-1/2 py-2.5 justify-center items-start gap-4">
-                        <x-markdown class="flex flex-col gap-3 text-muted text-xl xl:text-3xl font-normal">
+                    <div class="flex flex-col py-2.5 items-start gap-4">
+                        <div class="text-2xl font-bold">{{ __('home.about_me') }}</div>
+                        <h2 class="text-5xl xl:text-6xl font-normal mb-8">
+                            @if(!$aboutMeSection->isEmpty())
+                                {{ $aboutMeSection->first()->attractDescTransKey->getTranslation() }}
+                            @else
+                                Lorem ipsum dolor sit amet,
+                            @endif
+                        </h2>
+
+                        <x-markdown class="flex flex-col gap-3 text-muted text-xl xl:text-2xl font-normal">
                             @if(!$aboutMeSection->isEmpty())
                                 {{ $aboutMeSection->first()->contentTransKey->getTranslation() }}
                             @else
diff --git a/resources/views/public/legal-mentions.blade.php b/resources/views/public/legal-mentions.blade.php
new file mode 100644
index 0000000000000000000000000000000000000000..65d3c5cc42d7c0a94a6e7782337eb2535d96c795
--- /dev/null
+++ b/resources/views/public/legal-mentions.blade.php
@@ -0,0 +1,16 @@
+@extends('layouts.public', ['title' => __('generic.legal_mentions')])
+
+@section('content')
+    <x-public.navbar class="container mx-auto px-4"/>
+
+    <div class="container mx-auto px-4 py-24 flex flex-col gap-16">
+        <x-public.generic-page-header title="{{ __('generic.legal_mentions') }}"/>
+        <div>
+            <x-markdown class="markdown">
+                {{ $activeLegalMentionsText }}
+            </x-markdown>
+        </div>
+    </div>
+
+    <x-public.footer/>
+@endsection
\ No newline at end of file
diff --git a/resources/views/public/maintenance.blade.php b/resources/views/public/maintenance.blade.php
index 0df958413fd3073599f2ed770dc9887ff542ceb7..15da0114fd74c35f542237191938e6c3cf5a107c 100644
--- a/resources/views/public/maintenance.blade.php
+++ b/resources/views/public/maintenance.blade.php
@@ -1,4 +1,4 @@
-@extends('layouts.public', ['title' => 'Maintenance'])
+@extends('layouts.public', ['title' => 'Maintenance', 'description' => 'Site en construction...'])
 
 @section('content')
     <div class="inline-flex w-full min-h-screen flex-col justify-center items-center gap-2.5"
diff --git a/resources/views/public/portfolio-show.blade.php b/resources/views/public/portfolio-show.blade.php
index 9a8127f9722df173ee58f04ba33b39f773de5bca..bd3173a5e49d5173275150093e37c4031cea937a 100644
--- a/resources/views/public/portfolio-show.blade.php
+++ b/resources/views/public/portfolio-show.blade.php
@@ -1,4 +1,8 @@
-@extends('layouts.public', ['title' => $creation->nameTranslationKey->getTranslation()])
+@extends('layouts.public', [
+    'title' => $creation->nameTranslationKey->getTranslation(),
+    'description' => $creation->shortDescriptionTranslationKey->getTranslation(),
+    'image' => $creation->coverUploadedPicture
+])
 
 @section('content')
     <x-public.navbar class="container mx-auto px-4"/>
diff --git a/resources/views/public/portfolio.blade.php b/resources/views/public/portfolio.blade.php
index 11eb8b9f18bfd49d4f1d7269f0d2be519117f94a..5f001c4f7e15b224c42256fb73f56c73634e2ea6 100644
--- a/resources/views/public/portfolio.blade.php
+++ b/resources/views/public/portfolio.blade.php
@@ -1,4 +1,4 @@
-@extends('layouts.public', ['title' => __('navbar.portfolio')])
+@extends('layouts.public', ['title' => __('navbar.portfolio'), 'description' => __('services.description')])
 
 @section('content')
     <x-public.navbar class="container mx-auto px-4"/>
diff --git a/resources/views/public/prestation.blade.php b/resources/views/public/prestation.blade.php
index 7fd1dc6cba32231a337c539297609523825db2db..e998491d84ac3f4faee9d8a030233cde6eca272c 100644
--- a/resources/views/public/prestation.blade.php
+++ b/resources/views/public/prestation.blade.php
@@ -1,4 +1,8 @@
-@extends('layouts.public', ['title' => $prestation->nameTransKey->getTranslation()])
+@extends('layouts.public', [
+    'title' => $prestation->nameTransKey->getTranslation(),
+    'description' => $prestation->attractDescTransKey->getTranslation(),
+    'image' => $prestation->uploadedPicture
+])
 
 @section('content')
     <x-public.navbar class="container mx-auto px-4"/>
@@ -45,7 +49,7 @@ class="w-full relative aspect-square"/>
                 {{ __('generic.custom_needs_desc') }}
             </div>
         </div>
-        <x-public.button size="medium" tag="a" href="#">
+        <x-public.button size="medium" tag="a" href="{{ route('contact') }}">
             <div class="text-xl font-bold">{{ __('navbar.contact_me') }}</div>
         </x-public.button>
     </div>
diff --git a/resources/views/public/prestations.blade.php b/resources/views/public/prestations.blade.php
index 5d052fa588e10abe72c7c0f7580a09c50b906776..20b99d8a7fffe9978cb0085f02501ad9b0fc8614 100644
--- a/resources/views/public/prestations.blade.php
+++ b/resources/views/public/prestations.blade.php
@@ -1,4 +1,4 @@
-@extends('layouts.public', ['title' => __('navbar.services')])
+@extends('layouts.public', ['title' => __('navbar.services'), 'description' => __('services.attract_desc')])
 
 @section('content')
     <x-public.navbar class="container mx-auto px-4"/>
@@ -17,7 +17,7 @@
                 {{ __('generic.custom_needs_desc') }}
             </div>
         </div>
-        <x-public.button size="medium" tag="a" href="#">
+        <x-public.button size="medium" tag="a" href="{{ route('contact') }}">
             <div class="text-xl font-bold">{{ __('navbar.contact_me') }}</div>
         </x-public.button>
     </div>
diff --git a/resources/views/public/terms.blade.php b/resources/views/public/terms.blade.php
new file mode 100644
index 0000000000000000000000000000000000000000..c7d919ab651ef56b0fc097ccca6d36e41e0cdc9d
--- /dev/null
+++ b/resources/views/public/terms.blade.php
@@ -0,0 +1,16 @@
+@extends('layouts.public', ['title' => __('generic.terms')])
+
+@section('content')
+    <x-public.navbar class="container mx-auto px-4"/>
+
+    <div class="container mx-auto px-4 py-24 flex flex-col gap-16">
+        <x-public.generic-page-header title="{{ __('generic.terms') }}"/>
+        <div>
+            <x-markdown class="markdown">
+                {{ $activeTermsText }}
+            </x-markdown>
+        </div>
+    </div>
+
+    <x-public.footer/>
+@endsection
\ No newline at end of file
diff --git a/routes/console.php b/routes/console.php
index 0ef4bc96588dfaa08fe439c99b9ac6e9c86345ed..37195099b5989fd118ad261513cef434bea7a23b 100644
--- a/routes/console.php
+++ b/routes/console.php
@@ -9,6 +9,7 @@
 
 Schedule::command('save:requests')->everyMinute()->sentryMonitor();
 Schedule::command('process:user-agents')->hourly()->sentryMonitor();
+Schedule::command('process:ip-adresses')->everyFiveMinutes()->sentryMonitor();
 Schedule::command('flush:unused-uploaded-pictures')->daily()->sentryMonitor();
 
 Schedule::command('backup:clean --disable-notifications')->daily()->at('01:00');
diff --git a/routes/web.php b/routes/web.php
index aa637dc52583b1a3b5e1c9d22841990f31785ee2..3049d6c2b25ac7f4dede3368ce149133bca9d192 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -11,8 +11,10 @@
 use App\Http\Controllers\Admin\SocialMediaLinksController;
 use App\Http\Controllers\Admin\TermsSectionController;
 use App\Http\Controllers\Public\IndexController;
+use App\Http\Controllers\Public\LegalMentionsController;
 use App\Http\Controllers\Public\PortfolioController;
 use App\Http\Controllers\Public\PrestationController as PublicPrestationController;
+use App\Http\Controllers\Public\TermsController;
 use App\Http\Middleware\CheckPrivateModeMiddleware;
 use App\Http\Middleware\RedirectIfUserExistsMiddleware;
 use Illuminate\Support\Facades\Route;
@@ -29,6 +31,9 @@
     Route::get('/portfolio/api', [PortfolioController::class, 'api'])->name('portfolio.api');
     Route::get('/portfolio/{slug}', [PortfolioController::class, 'show'])->name('portfolio.show');
     Route::get('/prestations/{slug}', [PublicPrestationController::class, 'index'])->name('prestations.show');
+    Route::get('/mentions-legales', LegalMentionsController::class)->name('legal-mentions');
+    Route::get('/conditions-generales-de-vente', TermsController::class)->name('terms');
+    Route::view('/contact', 'public.contact')->name('contact');
 
     Route::view('/maintenance', 'public.maintenance')->name('maintenance');
 });
@@ -74,6 +79,8 @@
             ->name('delete');
         Route::delete('/{creation}/remove-addtionnal-image/{image}', [CreationController::class, 'removeAdditionalImage'])
             ->name('remove-additionnal-image');
+        Route::get('/{creation}/translate-with-ai', [CreationController::class, 'translateWithAi'])
+            ->name('translate-with-ai');
     });
 
     Route::prefix('social-media-links')->name('social-media-links.')->group(function () {
@@ -106,6 +113,8 @@
         Route::match(['GET', 'DELETE'], '/users/{id}/delete', [SettingsController::class, 'deleteUser'])->name('delete-user');
         Route::get('/general', [SettingsController::class, 'generalPage'])->name('general');
         Route::put('/general', [SettingsController::class, 'updateUserInfos'])->name('update-user-infos');
+        Route::get('/website-configuration', [SettingsController::class, 'websiteConfigPage'])->name('website-config');
+        Route::put('/website-configuration', [SettingsController::class, 'updateWebsiteConfig'])->name('update-website-config');
     });
 
     Route::prefix('prestations')->name('prestations.')->group(function () {
diff --git a/tests/Feature/Console/Command/FlushUnusedUploadedPicturesCommandTest.php b/tests/Feature/Console/Commands/FlushUnusedUploadedPicturesCommandTest.php
similarity index 86%
rename from tests/Feature/Console/Command/FlushUnusedUploadedPicturesCommandTest.php
rename to tests/Feature/Console/Commands/FlushUnusedUploadedPicturesCommandTest.php
index 0d30022ee60d508e8dbaf02a80e4fb216bda598c..c362a64e7c121bee5b6d32811e0b114a02823342 100644
--- a/tests/Feature/Console/Command/FlushUnusedUploadedPicturesCommandTest.php
+++ b/tests/Feature/Console/Commands/FlushUnusedUploadedPicturesCommandTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Tests\Feature\Console\Command;
+namespace Tests\Feature\Console\Commands;
 
 use App\Console\Commands\FlushUnusedUploadedPicturesCommand;
 use App\Models\UploadedPicture;
@@ -9,7 +9,8 @@
 use PHPUnit\Framework\Attributes\CoversClass;
 use Tests\TestCase;
 
-#[CoversClass(FlushUnusedUploadedPicturesCommand::class)] class FlushUnusedUploadedPicturesCommandTest extends TestCase
+#[CoversClass(FlushUnusedUploadedPicturesCommand::class)]
+class FlushUnusedUploadedPicturesCommandTest extends TestCase
 {
     use RefreshDatabase;
 
diff --git a/tests/Feature/Console/Command/OptimizeUploadedPicturesCommandTest.php b/tests/Feature/Console/Commands/OptimizeUploadedPicturesCommandTest.php
similarity index 89%
rename from tests/Feature/Console/Command/OptimizeUploadedPicturesCommandTest.php
rename to tests/Feature/Console/Commands/OptimizeUploadedPicturesCommandTest.php
index f9f09eefc283cf17693a50f4373d9818e0c10848..8863523704bf1d11e5b1cb36f88ba5cbdf012954 100644
--- a/tests/Feature/Console/Command/OptimizeUploadedPicturesCommandTest.php
+++ b/tests/Feature/Console/Commands/OptimizeUploadedPicturesCommandTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Tests\Feature\Console\Command;
+namespace Tests\Feature\Console\Commands;
 
 use App\Console\Commands\OptimizeUploadedPicturesCommand;
 use App\Models\UploadedPicture;
@@ -12,7 +12,8 @@
 /**
  * Tests de la commande d'optimisation des images uploadées
  */
-#[CoversClass(OptimizeUploadedPicturesCommand::class)] class OptimizeUploadedPicturesCommandTest extends TestCase
+#[CoversClass(OptimizeUploadedPicturesCommand::class)]
+class OptimizeUploadedPicturesCommandTest extends TestCase
 {
     use RefreshDatabase;
 
diff --git a/tests/Feature/Console/Commands/ProcessIpAdressesCommandTest.php b/tests/Feature/Console/Commands/ProcessIpAdressesCommandTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..87153e4c3ca3e11f8d6d9d78003014968bbadf11
--- /dev/null
+++ b/tests/Feature/Console/Commands/ProcessIpAdressesCommandTest.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Tests\Feature\Console\Commands;
+
+use App\Console\Commands\ProcessIpAdressesCommand;
+use App\Jobs\ProcessIpAddressesJob;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\Queue;
+use PHPUnit\Framework\Attributes\CoversClass;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+use Tests\TestCase;
+
+#[CoversClass(ProcessIpAdressesCommand::class)]
+class ProcessIpAdressesCommandTest extends TestCase
+{
+    use RefreshDatabase;
+
+    public function test_it_dispatches_job()
+    {
+        Queue::fake();
+        IpAddress::factory()->count(10)->create();
+
+        Artisan::call('process:ip-adresses');
+
+        Queue::assertPushed(ProcessIpAddressesJob::class);
+    }
+}
diff --git a/tests/Feature/Console/Commands/ProcessUserAgentsCommandTest.php b/tests/Feature/Console/Commands/ProcessUserAgentsCommandTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c6390dc1a8ee4b9cd4845a767bf1514ef6dbb4ae
--- /dev/null
+++ b/tests/Feature/Console/Commands/ProcessUserAgentsCommandTest.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Tests\Feature\Console\Commands;
+
+use App\Console\Commands\ProcessUserAgentsCommand;
+use App\Jobs\ProcessUserAgentJob;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Queue;
+use PHPUnit\Framework\Attributes\CoversClass;
+use SlProjects\LaravelRequestLogger\app\Models\UserAgent;
+use Tests\TestCase;
+
+#[CoversClass(ProcessUserAgentsCommand::class)]
+class ProcessUserAgentsCommandTest extends TestCase
+{
+    use RefreshDatabase;
+
+    public function test_it_dispatches_job()
+    {
+        Queue::fake();
+        UserAgent::factory()->count(10)->create();
+
+        $this->artisan('process:user-agents');
+
+        Queue::assertPushed(ProcessUserAgentJob::class, 10);
+    }
+}
diff --git a/tests/Feature/Console/Command/UpdateImageDimensionsTest.php b/tests/Feature/Console/Commands/UpdateImageDimensionsCommandTest.php
similarity index 92%
rename from tests/Feature/Console/Command/UpdateImageDimensionsTest.php
rename to tests/Feature/Console/Commands/UpdateImageDimensionsCommandTest.php
index df50701450a1af1058cbe8f37272250ab2420b43..99b45783e993c20d5a5d156556b09330626dc42c 100644
--- a/tests/Feature/Console/Command/UpdateImageDimensionsTest.php
+++ b/tests/Feature/Console/Commands/UpdateImageDimensionsCommandTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Tests\Feature\Console\Command;
+namespace Tests\Feature\Console\Commands;
 
 use App\Console\Commands\UpdateImageDimensionsCommand;
 use App\Models\UploadedPicture;
@@ -15,7 +15,8 @@
 /**
  * Tests de la commande de mise à jour des dimensions des images
  */
-#[CoversClass(UpdateImageDimensionsCommand::class)] class UpdateImageDimensionsTest extends TestCase
+#[CoversClass(UpdateImageDimensionsCommand::class)]
+class UpdateImageDimensionsCommandTest extends TestCase
 {
     use RefreshDatabase;
 
diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php
deleted file mode 100644
index 22ef0c599c0a71232c7b2b1ff5c5921e0392c50f..0000000000000000000000000000000000000000
--- a/tests/Feature/ExampleTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-namespace Tests\Feature;
-
-use Illuminate\Foundation\Testing\RefreshDatabase;
-use Tests\TestCase;
-
-class ExampleTest extends TestCase
-{
-    use RefreshDatabase;
-
-    /**
-     * A basic test example.
-     */
-    public function test_the_application_returns_a_successful_response(): void
-    {
-        $response = $this->get('/');
-
-        $response->assertStatus(200);
-    }
-}
diff --git a/tests/Feature/Jobs/ProcessIpAddressesJobTest.php b/tests/Feature/Jobs/ProcessIpAddressesJobTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..005713708d5713d40fe35eb84d601cc9b826f14c
--- /dev/null
+++ b/tests/Feature/Jobs/ProcessIpAddressesJobTest.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Tests\Feature\Jobs;
+
+use App\Jobs\ProcessIpAddressesJob;
+use App\Services\IpAddressMetadataResolverService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Http;
+use PHPUnit\Framework\Attributes\CoversClass;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+use Tests\TestCase;
+
+#[CoversClass(ProcessIpAddressesJob::class)]
+class ProcessIpAddressesJobTest extends TestCase
+{
+    use RefreshDatabase;
+
+    public function test_processes_ip_addresses_successfully()
+    {
+        $ip1 = IpAddress::factory()->create(['ip' => '208.80.152.201']);
+        $ip2 = IpAddress::factory()->create(['ip' => '24.48.0.1']);
+
+        Http::fake([
+            '*' => Http::response([
+                [
+                    'status' => 'success',
+                    'countryCode' => 'US',
+                    'lat' => 37.7892,
+                    'lon' => -122.402,
+                    'query' => '208.80.152.201',
+                ],
+                [
+                    'status' => 'success',
+                    'countryCode' => 'CA',
+                    'lat' => 45.6085,
+                    'lon' => -73.5493,
+                    'query' => '24.48.0.1',
+                ],
+            ]),
+        ]);
+
+        $job = new ProcessIpAddressesJob(collect([$ip1, $ip2]));
+        $job->handle(app(IpAddressMetadataResolverService::class));
+
+        $this->assertDatabaseHas('ip_address_metadata', [
+            'ip_address_id' => $ip1->id,
+            'country_code' => 'US',
+            'lat' => 37.7892,
+            'lon' => -122.402,
+        ]);
+
+        $this->assertDatabaseHas('ip_address_metadata', [
+            'ip_address_id' => $ip2->id,
+            'country_code' => 'CA',
+            'lat' => 45.6085,
+            'lon' => -73.5493,
+        ]);
+    }
+}
diff --git a/tests/Feature/Service/AiProviderServiceTest.php b/tests/Feature/Service/AiProviderServiceTest.php
index 4cd5d62a5e19ca3c658c2a4fb3a9a3918fb44b02..1de43af3942ac979ba9f366f06300529db396eb9 100644
--- a/tests/Feature/Service/AiProviderServiceTest.php
+++ b/tests/Feature/Service/AiProviderServiceTest.php
@@ -60,7 +60,7 @@ public function test_prompt_with_pictures_sends_correct_request()
     {
         Storage::fake('public');
         Http::fake([
-            'https://api.test-provider.com' => Http::response(json_encode($this->sampleResponse)),
+            'https://api.test-provider.com' => Http::response($this->sampleResponse),
         ]);
 
         $mockTranscodingService = Mockery::mock(ImageTranscodingService::class);
@@ -91,7 +91,7 @@ public function test_prompt_with_pictures_sends_correct_request()
     public function test_prompt_sends_correct_request()
     {
         Http::fake([
-            'https://api.test-provider.com' => Http::response(json_encode($this->sampleResponse)),
+            'https://api.test-provider.com' => Http::response($this->sampleResponse),
         ]);
 
         Config::set('ai-provider.selected-provider', 'test-provider');
@@ -116,7 +116,7 @@ public function test_prompt_with_pictures_handles_transcoding_failure()
     {
         Storage::fake('public');
         Http::fake([
-            'https://api.test-provider.com' => Http::response(json_encode($this->sampleResponse)),
+            'https://api.test-provider.com' => Http::response($this->sampleResponse),
         ]);
 
         $mockTranscodingService = Mockery::mock(ImageTranscodingService::class);
diff --git a/tests/Feature/Service/IpAddressMetadataResolverServiceTest.php b/tests/Feature/Service/IpAddressMetadataResolverServiceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..dcb6c86b358cbf6c93a836a1711927efbfc5e395
--- /dev/null
+++ b/tests/Feature/Service/IpAddressMetadataResolverServiceTest.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Tests\Feature\Service;
+
+use App\Services\IpAddressMetadataResolverService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Exceptions;
+use Illuminate\Support\Facades\Http;
+use PHPUnit\Framework\Attributes\CoversClass;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+use Tests\TestCase;
+
+#[CoversClass(IpAddressMetadataResolverService::class)]
+class IpAddressMetadataResolverServiceTest extends TestCase
+{
+    use RefreshDatabase;
+
+    public function test_resolves_ip_addresses_successfully()
+    {
+        Exceptions::fake();
+
+        Http::fake([
+            '*' => Http::response([
+                [
+                    'status' => 'success',
+                    'countryCode' => 'US',
+                    'lat' => 37.7892,
+                    'lon' => -122.402,
+                    'query' => '208.80.152.201',
+                ],
+                [
+                    'status' => 'success',
+                    'countryCode' => 'CA',
+                    'lat' => 45.6085,
+                    'lon' => -73.5493,
+                    'query' => '24.48.0.1',
+                ],
+            ]),
+        ]);
+
+        $service = new IpAddressMetadataResolverService;
+
+        $ipAddresses = IpAddress::factory()->count(2)->create();
+        $result = $service->resolve($ipAddresses);
+
+        Exceptions::assertNothingReported();
+        $this->assertCount(2, $result);
+        $this->assertEquals('US', $result[0]['countryCode']);
+        $this->assertEquals('CA', $result[1]['countryCode']);
+    }
+}