| app | ||
| bootstrap | ||
| config | ||
| database | ||
| docker/nginx | ||
| public | ||
| resources | ||
| routes | ||
| storage | ||
| tests | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.example | ||
| .env.production.example | ||
| .gitattributes | ||
| .gitignore | ||
| .npmrc | ||
| artisan | ||
| composer.json | ||
| composer.lock | ||
| DEPLOY.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| phpunit.xml | ||
| README.md | ||
| vite.config.js | ||
vivien-lab.fr
Site personnel de Vivien Joly — concepteur-développeur full-stack. Carte de visite professionnelle (Accueil · Savoir-faire · CV · Contact) construite en SPA Vue 3 + API Laravel 12, full-Dockerisée.
Sommaire
- Aperçu
- Stack technique
- Architecture
- Fonctionnalités
- URLs
- Installation locale
- Déploiement sur le VPS
- Maintenance
- Sécurité
- Pistes futures
Aperçu
Site vitrine + carte de visite présentant les compétences, le parcours et les services d'un développeur freelance. Le site est :
- 100 % SPA (Vue Router en
createWebHistory, pas de rechargement entre les pages) - Bilingue FR / EN (UI + données API + CV)
- Dark / Light avec auto-détection
prefers-color-scheme+ persistance - Dynamique côté serveur (API JSON Laravel) et côté front (composables Vue, animations scroll, status live, toasts)
- Sécurisé (CSRF, rate limiting, honeypot, headers prod, validation stricte, anti email-injection)
- Auto-hébergé sur VPS avec Docker, Forgejo (git), Umami (analytics)
Stack technique
Backend
| Outil | Rôle |
|---|---|
| Laravel 12 | Framework PHP, routing, validation, mailing |
| PHP 8.4 | Runtime |
| SQLite | Sessions, cache (changeable en MariaDB en prod) |
| Symfony Mailer (via Laravel) | Envoi email SMTP |
Frontend
| Outil | Rôle |
|---|---|
| Vue 3 (Composition API) | Framework UI |
| Vue Router 4 | Routing client-side (mode HTML5 History) |
| vue-i18n 9 | Multilingue |
| axios | HTTP client (avec CSRF auto) |
| Vite | Bundler / dev server / HMR |
Infra
| Outil | Rôle |
|---|---|
| Docker Compose | Orchestration des services |
| Nginx Alpine | Reverse-proxy + serveur statique |
| PHP-FPM 8.4 Alpine | Runtime PHP |
| Mailpit | Mailcatcher local (dev only) |
| Umami + Postgres | Analytics self-hosted |
| Forgejo (externe) | Git + CI/CD |
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Navigateur │
│ └─ SPA Vue 3 (router, i18n, composables, theme) │
└──────────────────┬──────────────────────────────────────────┘
│ HTTPS
┌──────────────────▼──────────────────────────────────────────┐
│ Nginx (reverse-proxy) │
│ ├─ / → app.blade.php (shell SPA) │
│ ├─ /api/home, /savoir-faire, /cv → JSON │
│ ├─ /api/contact (POST) → ContactController + Mailer │
│ ├─ /cv_Vivien_JOLY.pdf → PDF static │
│ └─ /up → health check (status dot live)│
└──────────────────┬──────────────────────────────────────────┘
│ FastCGI
┌──────────────────▼──────────────────────────────────────────┐
│ PHP-FPM (Laravel 12) │
│ ├─ Routes web.php : SPA shell + /api/* + assets │
│ ├─ SiteController : sert site.{fr,en}.php (cache 5 min) │
│ ├─ ContactController : validate + Mailable + SMTP │
│ ├─ SecurityHeaders middleware : HSTS + CSP + nosniff │
│ └─ Storage : SQLite (sessions, cache, jobs) │
└─────────────────────────────────────────────────────────────┘
Source de vérité du contenu
Pas de DB métier — le contenu est statique dans deux fichiers PHP :
database/data/site.fr.php— version françaisedatabase/data/site.en.php— version anglaise
Édition simple, versionné en git, cache Laravel 5 min.
Fonctionnalités
Pages
| Route | Composant | Description |
|---|---|---|
/ |
Home.vue |
Hero + compteur années d'expérience animé + CTA |
/savoir-faire |
SavoirFaire.vue |
Stack par catégories, prestations, méthode (5 étapes), témoignages clients |
/cv |
CV.vue |
3 sections alternées (XP / Formation / Références) + bouton « Télécharger PDF » |
/contact |
Contact.vue |
Channels (email, LinkedIn) + formulaire dynamique |
Comportements dynamiques
- Status dot live : ping
/uptoutes les 30 s → vert / orange (lent) / rouge (down) - Compteur d'expérience : calcul depuis octobre 2020, animation easeOutCubic au mount
- Scroll reveal :
IntersectionObserverqui ajoute.reveal-inaux éléments[data-reveal] - Skeletons : pendant le fetch initial des API
- Toasts : succès / erreur / info avec animations
- Transitions de pages : fade entre routes
- Toggle FR/EN :
localStorage+ auto-detect navigateur - Toggle dark/light :
localStorage+ auto-detectprefers-color-scheme
API JSON
| Endpoint | Méthode | Description |
|---|---|---|
/api/home?lang=fr|en |
GET | Données page d'accueil |
/api/savoir-faire?lang=fr|en |
GET | Stack, prestations, méthode, retours |
/api/cv?lang=fr|en |
GET | Expériences, formations, références |
/api/contact |
POST | Envoi du formulaire (CSRF + throttle 5/min + honeypot) |
/up |
GET | Health check Laravel |
Cache HTTP public, max-age=300 + cache Laravel 5 min sur les GET.
PDF du CV
Bouton « Télécharger PDF » sur /cv → /cv_Vivien_JOLY.pdf (787 Ko, fichier statique servi par Nginx).
URLs
Développement local
| URL | Service |
|---|---|
| http://localhost:8000 | Laravel + SPA |
| http://localhost:5173 | Vite dev server (HMR) |
| http://localhost:8025 | Mailpit — voir les mails interceptés |
| http://localhost:3000 | Umami — analytics |
| http://localhost:1025 | SMTP Mailpit (utilisé par Laravel) |
Production (à configurer côté DNS)
| URL prévue | Service |
|---|---|
| https://vivien-lab.fr | Site principal |
| https://git.vivien-lab.fr | Forgejo (gitea fork) |
| https://analytics.vivien-lab.fr | Umami |
Installation locale
Prérequis
- Linux / macOS / WSL
- Docker + Docker Compose
Aucun PHP, Composer, Node ou autre n'est requis sur la machine hôte — tout passe par Docker.
Lancement
git clone https://git.vivien-lab.fr/vivien-lab.fr.git
cd vivien-lab.fr
# Build de l'image PHP custom
docker compose --env-file .env.docker build app
# Lancement de tous les services
docker compose --env-file .env.docker up -d
Au premier lancement, les images Docker sont téléchargées (~ 5 min).
Le service node lance automatiquement npm install puis npm run dev (Vite avec HMR).
Vérification
curl -I http://localhost:8000 # → 200 OK
curl http://localhost:8000/up # → page de health
Ouvrir http://localhost:8000 dans le navigateur.
Mise à jour des dépendances
docker exec -u 1000:1000 vivienlab_app composer update
docker exec -u 1000:1000 vivienlab_node npm update
Déploiement sur le VPS
1. Prérequis serveur
- Linux (Debian/Ubuntu/Alpine)
- Docker + Docker Compose
- Domaine
vivien-lab.frpointant vers le VPS (record DNS A/AAAA) - Reverse-proxy en front : Caddy (recommandé pour HTTPS auto via Let's Encrypt) ou Traefik
2. Récupération du code
git clone https://git.vivien-lab.fr/vivien-lab.fr.git
cd vivien-lab.fr
3. Configuration .env production
cp .env.production.example .env
nano .env
À renseigner :
APP_KEY= # généré ci-dessous
APP_URL=https://vivien-lab.fr
APP_DEBUG=false
APP_ENV=production
# Vraie SMTP (Brevo, Mailgun, OVH, Gmail App Password…)
MAIL_MAILER=smtp
MAIL_HOST=smtp.brevo.com
MAIL_PORT=587
MAIL_USERNAME=votre_login
MAIL_PASSWORD=votre_clé
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="contact@vivien-lab.fr"
MAIL_TO_ADDRESS=
# Cookies en HTTPS only
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=strict
# Analytics (vide = désactivé)
UMAMI_SCRIPT_URL=https://analytics.vivien-lab.fr/script.js
UMAMI_WEBSITE_ID=
4. Génération de l'APP_KEY
docker run --rm -v "$(pwd):/app" -w /app composer:2 install --no-dev --optimize-autoloader
docker compose --env-file .env.docker build app
docker compose --env-file .env.docker run --rm app php artisan key:generate
5. Permissions
docker run --rm -v "$(pwd):/app" alpine sh -c "chown -R 1000:1000 /app && chmod -R 775 /app/storage /app/bootstrap/cache"
6. Build des assets Vite
docker run --rm -v "$(pwd):/app" -w /app -u 1000:1000 node:20-alpine sh -c "npm ci && npm run build"
7. Lancement (sans Mailpit en prod)
docker compose --env-file .env.docker up -d app nginx
docker compose --env-file .env.docker up -d umami_db umami # si analytics voulu
8. Migrations + cache Laravel
docker compose exec app php artisan migrate --force
docker compose exec app php artisan config:cache
docker compose exec app php artisan route:cache
docker compose exec app php artisan view:cache
9. Reverse-proxy (Caddy exemple)
/etc/caddy/Caddyfile :
vivien-lab.fr {
reverse_proxy localhost:8000
}
git.vivien-lab.fr {
reverse_proxy localhost:3001 # à adapter selon votre setup Forgejo
}
analytics.vivien-lab.fr {
reverse_proxy localhost:3000
}
sudo systemctl reload caddy
Caddy gère automatiquement HTTPS (Let's Encrypt) — aucune autre config requise.
10. Vérification post-déploiement
curl -I https://vivien-lab.fr # 200 + HSTS + CSP visibles
curl https://vivien-lab.fr/up # health check
curl https://vivien-lab.fr/api/home # API JSON
Vérifier dans DevTools :
Strict-Transport-SecurityprésentContent-Security-PolicyprésentServer: nginx(pas de version)- Cookies en
Secure; SameSite=Strict
Audit externe : passer le domaine à securityheaders.com et viser A+.
Maintenance
Mise à jour du contenu
Éditer database/data/site.fr.php ou site.en.php, commit, push.
docker compose exec app php artisan cache:clear
Mise à jour du PDF du CV
Remplacer public/cv_Vivien_JOLY.pdf, commit, push. Aucun rebuild nécessaire (servi en static par Nginx).
Déploiement d'une mise à jour
git pull
docker run --rm -v "$(pwd):/app" -w /app composer:2 install --no-dev --optimize-autoloader
docker run --rm -v "$(pwd):/app" -w /app -u 1000:1000 node:20-alpine sh -c "npm ci && npm run build"
docker compose exec app php artisan migrate --force
docker compose exec app php artisan config:cache route:cache view:cache
docker compose restart app nginx
Backups à automatiser
| À sauvegarder | Localisation |
|---|---|
| Sessions/cache Laravel | database/database.sqlite |
| Analytics Umami | volume Docker umami_db_data |
| PDF du CV | public/cv_Vivien_JOLY.pdf |
| Contenu du site | database/data/site.{fr,en}.php |
Cron suggéré : dump quotidien de la SQLite + pg_dump du volume Umami → upload vers stockage off-site.
Logs
docker compose logs -f app # PHP-FPM + Laravel
docker compose logs -f nginx # accès web
storage/logs/laravel.log contient les erreurs Laravel.
Sécurité
Protections actives
| Couche | Mécanisme |
|---|---|
| Formulaire | CSRF (Laravel middleware web) |
| Formulaire | Rate limiting (5 requêtes/min/IP) |
| Formulaire | Honeypot (champ website caché) |
| Formulaire | Validation stricte (longueurs, email RFC, regex anti-\r\n) |
| Mailer | Email header injection bloquée (anti-CRLF dans subject/name) |
| HTTP | X-Frame-Options: SAMEORIGIN |
| HTTP | X-Content-Type-Options: nosniff |
| HTTP | Referrer-Policy: strict-origin-when-cross-origin |
| HTTP | Permissions-Policy: geolocation=(), microphone=(), camera=() |
| HTTP (prod) | Strict-Transport-Security (HSTS, 1 an, includeSubDomains, preload) |
| HTTP (prod) | Content-Security-Policy stricte (default-src 'self') |
| Sessions | http_only + same_site=strict + secure=true (prod) |
| Nginx | server_tokens off |
| Vue | Échappement automatique de {{ }} (XSS) |
| API | Aucun SQL brut (Eloquent + validator builder) |
| Dépendances | composer audit + npm audit → 0 vulnérabilité |
Checklist déploiement
APP_DEBUG=false(sinon stack trace exposée en cas d'erreur)APP_ENV=production(active HSTS + CSP via SecurityHeaders middleware)APP_KEYrégénéré (jamais commit)- HTTPS forcé via reverse-proxy
SESSION_SECURE_COOKIE=trueSESSION_SAME_SITE=strict- SMTP réel configuré (sinon
/api/contactplante) - Credentials Umami changés (
UMAMI_DB_PASSWORD,UMAMI_APP_SECRET) - Backups planifiés (cron)
- Fail2ban configuré sur les access logs Nginx
- Headers vérifiés via securityheaders.com
Pistes futures
| Idée | Effort | Pertinence |
|---|---|---|
Open Graph + sitemap.xml + JSON-LD Person |
30 min | ⭐⭐⭐ partage LinkedIn propre |
| Tests Pest (back) + Vitest (front) | 1-2 h | ⭐⭐ qualité démontrable |
| CI/CD via Forgejo Actions | 1-2 h | ⭐⭐ déploiement auto |
| Mini blog Markdown | 2 h | ⭐ articles tech, SEO |
| PWA (manifest + service worker) | 1 h | ⭐ installable offline |
| Audit Lighthouse + corrections | 1 h | ⭐ perfo, a11y |
Crédits
- Design : sombre minimal custom, accent vert
#4ade80(dark) /#16a34a(light) - Polices : Inter + JetBrains Mono
- Stack : Laravel 12 + Vue 3 + Vite, sous Docker
- Hébergement : VPS auto-hébergé
Auteur : Vivien Joly — LinkedIn