Site portfolio Laravel + Vue.js
Find a file
2026-05-11 21:55:16 +02:00
app Initial commit 2026-05-11 21:55:16 +02:00
bootstrap Initial commit 2026-05-11 21:55:16 +02:00
config Initial commit 2026-05-11 21:55:16 +02:00
database Initial commit 2026-05-11 21:55:16 +02:00
docker/nginx Initial commit 2026-05-11 21:55:16 +02:00
public Initial commit 2026-05-11 21:55:16 +02:00
resources Initial commit 2026-05-11 21:55:16 +02:00
routes Initial commit 2026-05-11 21:55:16 +02:00
storage Initial commit 2026-05-11 21:55:16 +02:00
tests Initial commit 2026-05-11 21:55:16 +02:00
.dockerignore Initial commit 2026-05-11 21:55:16 +02:00
.editorconfig Initial commit 2026-05-11 21:55:16 +02:00
.env.example Initial commit 2026-05-11 21:55:16 +02:00
.env.production.example Initial commit 2026-05-11 21:55:16 +02:00
.gitattributes Initial commit 2026-05-11 21:55:16 +02:00
.gitignore Initial commit 2026-05-11 21:55:16 +02:00
.npmrc Initial commit 2026-05-11 21:55:16 +02:00
artisan Initial commit 2026-05-11 21:55:16 +02:00
composer.json Initial commit 2026-05-11 21:55:16 +02:00
composer.lock Initial commit 2026-05-11 21:55:16 +02:00
DEPLOY.md Initial commit 2026-05-11 21:55:16 +02:00
docker-compose.yml Initial commit 2026-05-11 21:55:16 +02:00
Dockerfile Initial commit 2026-05-11 21:55:16 +02:00
package-lock.json Initial commit 2026-05-11 21:55:16 +02:00
package.json Initial commit 2026-05-11 21:55:16 +02:00
phpunit.xml Initial commit 2026-05-11 21:55:16 +02:00
README.md Initial commit 2026-05-11 21:55:16 +02:00
vite.config.js Initial commit 2026-05-11 21:55:16 +02:00

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

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çaise
  • database/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 /up toutes les 30 s → vert / orange (lent) / rouge (down)
  • Compteur d'expérience : calcul depuis octobre 2020, animation easeOutCubic au mount
  • Scroll reveal : IntersectionObserver qui ajoute .reveal-in aux é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-detect prefers-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.fr pointant 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-Security présent
  • Content-Security-Policy présent
  • Server: 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_KEY régénéré (jamais commit)
  • HTTPS forcé via reverse-proxy
  • SESSION_SECURE_COOKIE=true
  • SESSION_SAME_SITE=strict
  • SMTP réel configuré (sinon /api/contact plante)
  • 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 JolyLinkedIn