SPB Git

spb/phi-conseil

Public

PHI Conseil : site Next.js 16 fidèle à la maquette (accueil 7 sections, pages internes, contenu JSON validé, formulaire de contact sécurisé, tests, docs)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 8, 2026) parent 0d135ae Browse files

90 changed files +5,294 −139

90 changed files
added .github/workflows/ci.yml +37 −0
@@ -0,0 +1,37 @@
1 +name: CI
2 +
3 +on:
4 + push:
5 + branches: [main]
6 + pull_request:
7 +
8 +jobs:
9 + check:
10 + runs-on: ubuntu-latest
11 + env:
12 + CONTACT_TRANSPORT: log
13 + NEXT_PUBLIC_SITE_URL: http://127.0.0.1:3100
14 + steps:
15 + - uses: actions/checkout@v4
16 + - uses: pnpm/action-setup@v4
17 + with:
18 + version: 11
19 + - uses: actions/setup-node@v4
20 + with:
21 + node-version: 22
22 + cache: pnpm
23 + - run: pnpm install --frozen-lockfile
24 + - run: pnpm lint
25 + - run: pnpm typecheck
26 + - run: pnpm test
27 + - run: pnpm build
28 + - run: pnpm exec playwright install --with-deps chromium webkit
29 + - run: pnpm test:e2e
30 + - uses: actions/upload-artifact@v4
31 + if: always()
32 + with:
33 + name: playwright-report
34 + path: |
35 + playwright-report
36 + docs/screenshots
37 + retention-days: 14
modified .gitignore +7 −0
@@ -39,3 +39,10 @@ yarn-error.log*
39 39 # typescript
40 40 *.tsbuildinfo
41 41 next-env.d.ts
42 +
43 +# tests & captures
44 +/test-results/
45 +/playwright-report/
46 +/docs/screenshots/*.png
47 +!/docs/screenshots/.gitkeep
48 +.env.local
added .prettierignore +6 −0
@@ -0,0 +1,6 @@
1 +.next
2 +node_modules
3 +pnpm-lock.yaml
4 +test-results
5 +playwright-report
6 +docs/screenshots
added .prettierrc +7 −0
@@ -0,0 +1,7 @@
1 +{
2 + "semi": true,
3 + "singleQuote": false,
4 + "trailingComma": "all",
5 + "printWidth": 100,
6 + "plugins": ["prettier-plugin-tailwindcss"]
7 +}
modified README.md +41 −22
@@ -1,36 +1,55 @@
1 −This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
1 +# PHI Conseil — site web
2 2
3 −## Getting Started
3 +Site vitrine de PHI Conseil (« Stratégies · Financement · Réussite »), reproduit fidèlement à partir de la maquette client (`references/phi-conseil-homepage.jpeg`) et conçu comme un produit de production : Next.js 16 (App Router), React 19, TypeScript strict, Tailwind CSS 4, contenu administrable validé par Zod, formulaire de contact sécurisé, tests unitaires et de bout en bout.
4 4
5 −First, run the development server:
5 +## Démarrage
6 6
7 7 ```bash
8 −npm run dev
9 −# or
10 −yarn dev
11 −# or
12 −pnpm dev
13 −# or
14 −bun dev
8 +pnpm install
9 +cp .env.example .env.local # ajuster au besoin (transport courriel, URL)
10 +pnpm dev # http://localhost:3000
15 11 ```
16 12
17 −Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
13 +Prérequis : Node.js ≥ 20 (testé avec 26), pnpm ≥ 10.
18 14
19 −You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
15 +## Scripts
20 16
21 −This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
17 +| Commande | Rôle |
18 +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
19 +| `pnpm dev` | serveur de développement |
20 +| `pnpm build` / `pnpm start` | build de production / serveur de production (`-p <port>`) |
21 +| `pnpm lint` | ESLint (config Next + règles React Compiler) |
22 +| `pnpm typecheck` | `tsc --noEmit` |
23 +| `pnpm test` | tests unitaires Vitest (schémas, validation, sécurité, API, composants) |
24 +| `pnpm test:e2e` | Playwright : navigation, menu mobile, formulaire, débordements, axe, captures (`docs/screenshots/`) ; nécessite `pnpm build` au préalable et `pnpm exec playwright install chromium webkit` la première fois |
25 +| `pnpm check` | lint + typecheck + tests + build |
26 +| `pnpm icons` | régénère favicon / icônes à partir du logo (spirale dorée) |
27 +| `pnpm format` | Prettier |
22 28
23 −## Learn More
29 +## Structure
24 30
25 −To learn more about Next.js, take a look at the following resources:
26 −
27 −- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 −- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
31 +```
32 +content/ contenu administrable (site.json, home.json, pages.json) — voir docs/CLIENT_GUIDE.md
33 +src/app/ routes (accueil, services, a-propos, temoignages, nous-joindre, confidentialite, api/contact)
34 +src/components/ layout/ sections/ ui/ graphics/ forms/
35 +src/lib/ content.ts (schémas + chargeur), env.ts, validation.ts, security.ts, mail.ts, seo.ts, spiral.ts
36 +src/styles/ tokens.css (design tokens : couleurs échantillonnées, typo, espacements…)
37 +tests/unit, tests/e2e
38 +docs/ plan, déploiement, guide client, rapport QA, registre des actifs, captures
39 +references/ maquette client
40 +```
29 41
30 −You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
42 +## Principes
31 43
32 −## Deploy on Vercel
44 +- **Fidélité** : couleurs échantillonnées sur la maquette, structure DOM = ordre visuel sur tous les écrans, motifs (spirale de Fibonacci, vagues, feuillage) en SVG.
45 +- **Honnêteté du contenu** : aucune coordonnée, témoignage ou réalisation inventés. Les témoignages ne sont publiés que s'ils sont `approved: true` et la section `visible: true` ; les photos de la maquette sont remplacées par des illustrations vectorielles tant que le client n'a pas fourni d'images sous licence (`docs/ASSET_REGISTER.md`).
46 +- **Sécurité** : CSP, en-têtes durcis, validation Zod client + serveur, honeypot, délai anti-robot, limitation de débit, dédoublonnage, journalisation sans donnée personnelle, secrets uniquement côté serveur.
47 +- **Accessibilité** : WCAG 2.2 AA visé (axe sans violation sur toutes les pages), navigation clavier, `prefers-reduced-motion`, lien d'évitement, états focus visibles.
33 48
34 −The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
49 +## Documentation
35 50
36 −Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
51 +- `docs/IMPLEMENTATION_PLAN.md` — audit, architecture, décisions, critères d'acceptation
52 +- `docs/DEPLOYMENT.md` — hébergement, DNS, HTTPS, variables, sauvegardes, rollback
53 +- `docs/CLIENT_GUIDE.md` — modifier textes, services, témoignages, images, coordonnées
54 +- `docs/QA_REPORT.md` — résultats réels des vérifications, limites, points à valider
55 +- `docs/ASSET_REGISTER.md` — provenance, licence et statut de chaque actif
added content/home.json +80 −0
@@ -0,0 +1,80 @@
1 +{
2 + "hero": {
3 + "titleLines": ["Transformer", "vos idées en projets", "concrets"],
4 + "subtitle": "Votre partenaire en recherche de subventions, en rédaction de demandes et en reddition de comptes.",
5 + "cta": { "label": "Découvrir nos services", "href": "/services" },
6 + "image": {
7 + "src": "",
8 + "alt": "Bureau lumineux avec une plante, une tasse et un ordinateur portable"
9 + }
10 + },
11 + "services": {
12 + "visible": true,
13 + "title": "Nos services",
14 + "items": [
15 + {
16 + "slug": "recherche-de-subventions",
17 + "icon": "search",
18 + "title": "Recherche de subventions",
19 + "summary": "Trouvez les programmes adaptés à vos projets.",
20 + "description": ""
21 + },
22 + {
23 + "slug": "redaction-et-revision-de-demandes",
24 + "icon": "document-edit",
25 + "title": "Rédaction et révision de demandes",
26 + "summary": "Présentez des projets solides et convaincants.",
27 + "description": ""
28 + },
29 + {
30 + "slug": "redaction-et-revision-de-rapports",
31 + "icon": "chart",
32 + "title": "Rédaction et révision de rapports",
33 + "summary": "Communiquez efficacement vos résultats.",
34 + "description": ""
35 + },
36 + {
37 + "slug": "reddition-de-comptes",
38 + "icon": "check-circle",
39 + "title": "Reddition de comptes",
40 + "summary": "Assurez une gestion rigoureuse et conforme.",
41 + "description": ""
42 + }
43 + ]
44 + },
45 + "about": {
46 + "visible": true,
47 + "overtitle": "À propos de PHI Conseil",
48 + "title": "Une approche stratégique pour maximiser vos opportunités",
49 + "paragraph": "Forte d'une expertise en financement et en gestion de projets, Phi Conseil vous accompagne avec rigueur, créativité et bienveillance, pour donner vie à vos ambitions.",
50 + "cta": { "label": "En savoir plus", "href": "/a-propos" },
51 + "benefits": [
52 + "Une expertise reconnue",
53 + "Une approche personnalisée",
54 + "Un accompagnement de A à Z",
55 + "Un engagement envers votre réussite"
56 + ],
57 + "image": {
58 + "src": "",
59 + "alt": "Vase transparent avec feuillage posé sur des livres, lumière douce"
60 + }
61 + },
62 + "testimonials": {
63 + "visible": false,
64 + "previewOnly": true,
65 + "items": [
66 + {
67 + "quote": "« Grâce à l'accompagnement de Phi Conseil, nous avons obtenu un financement majeur pour notre projet. Leur professionnalisme et leur disponibilité font toute la différence. »",
68 + "author": "Exemple de maquette (non vérifié)",
69 + "role": "Témoignage fictif de la capture — à remplacer par un avis autorisé",
70 + "approved": false
71 + }
72 + ]
73 + },
74 + "callToAction": {
75 + "visible": true,
76 + "title": "Parlons de vos projets !",
77 + "subtitle": "Contactez-nous dès aujourd'hui pour discuter de vos besoins.",
78 + "cta": { "label": "Nous joindre", "href": "/nous-joindre" }
79 + }
80 +}
added content/pages.json +42 −0
@@ -0,0 +1,42 @@
1 +{
2 + "services": {
3 + "title": "Nos services",
4 + "intro": "Quatre services complémentaires pour financer, documenter et rendre compte de vos projets.",
5 + "seoDescription": "Recherche de subventions, rédaction et révision de demandes et de rapports, reddition de comptes : les services de PHI Conseil."
6 + },
7 + "about": {
8 + "title": "À propos",
9 + "intro": "Une approche stratégique pour maximiser vos opportunités.",
10 + "seoDescription": "Découvrez l'approche de PHI Conseil : expertise, personnalisation, accompagnement de A à Z et engagement envers votre réussite.",
11 + "body": [
12 + "Forte d'une expertise en financement et en gestion de projets, Phi Conseil vous accompagne avec rigueur, créativité et bienveillance, pour donner vie à vos ambitions."
13 + ]
14 + },
15 + "testimonials": {
16 + "title": "Témoignages",
17 + "intro": "Les témoignages publiés sur cette page sont ceux de clients ayant autorisé leur diffusion.",
18 + "emptyMessage": "Aucun témoignage n'est publié pour le moment.",
19 + "seoDescription": "Témoignages de clients de PHI Conseil."
20 + },
21 + "contact": {
22 + "title": "Nous joindre",
23 + "intro": "Décrivez-nous votre projet ou votre besoin : nous vous répondrons dans les meilleurs délais.",
24 + "seoDescription": "Contactez PHI Conseil pour discuter de vos besoins en recherche de subventions, rédaction de demandes et reddition de comptes.",
25 + "subjects": [
26 + "Recherche de subventions",
27 + "Rédaction ou révision d'une demande",
28 + "Rédaction ou révision d'un rapport",
29 + "Reddition de comptes",
30 + "Autre demande"
31 + ],
32 + "consentLabel": "J'accepte que mes renseignements soient utilisés pour traiter ma demande, conformément à la politique de confidentialité.",
33 + "successTitle": "Message envoyé",
34 + "successMessage": "Merci ! Votre message a bien été transmis. Nous vous répondrons dans les meilleurs délais."
35 + },
36 + "privacy": {
37 + "title": "Politique de confidentialité et mentions légales",
38 + "seoDescription": "Politique de confidentialité et mentions légales de PHI Conseil.",
39 + "lastUpdated": "2026-11-08",
40 + "reviewed": false
41 + }
42 +}
added content/site.json +36 −0
@@ -0,0 +1,36 @@
1 +{
2 + "brand": {
3 + "name": "PHI Conseil",
4 + "tagline": "STRATÉGIES · FINANCEMENT · RÉUSSITE",
5 + "footerSignature": "DES STRATÉGIES QUI DONNENT VIE À VOS PROJETS.",
6 + "legalName": "Phi Conseil"
7 + },
8 + "navigation": [
9 + { "label": "Accueil", "href": "/" },
10 + { "label": "Nos services", "href": "/services" },
11 + { "label": "À propos", "href": "/a-propos" },
12 + { "label": "Témoignages", "href": "/temoignages" },
13 + { "label": "Nous joindre", "href": "/nous-joindre" }
14 + ],
15 + "headerCta": { "label": "Contactez-nous", "href": "/nous-joindre" },
16 + "contact": {
17 + "email": "",
18 + "phone": "",
19 + "address": "",
20 + "region": "Québec, Canada"
21 + },
22 + "social": {
23 + "linkedin": ""
24 + },
25 + "legalLinks": [
26 + { "label": "Confidentialité", "href": "/confidentialite" },
27 + { "label": "Mentions légales", "href": "/confidentialite#mentions-legales" }
28 + ],
29 + "seo": {
30 + "siteUrl": "https://www.example.invalid",
31 + "defaultTitle": "PHI Conseil — Stratégies, financement, réussite",
32 + "titleTemplate": "%s | PHI Conseil",
33 + "defaultDescription": "Votre partenaire en recherche de subventions, en rédaction de demandes et en reddition de comptes.",
34 + "locale": "fr_CA"
35 + }
36 +}
added docs/ASSET_REGISTER.md +18 −0
@@ -0,0 +1,18 @@
1 +# Registre des actifs — PHI Conseil
2 +
3 +| Actif | Emplacement | Provenance | Licence | Statut |
4 +| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------- |
5 +| Maquette de l'accueil | `references/phi-conseil-homepage.jpeg` | fournie par le client (IMG_1079.JPG, 1024 × 1536) | usage interne de référence | référence, non publiée |
6 +| Logo (spirale dorée + rectangle d'or) | `src/components/graphics/Logo.tsx`, `src/lib/spiral.ts`, `public/logo-mark.svg` | **recréé en SVG** d'après la maquette (construction Fibonacci 1-1-2-3-5-8-13-21) | œuvre dérivée de l'identité du client | ⚠️ à valider par le client ; remplacer par le vectoriel officiel s'il existe |
7 +| Favicon / icônes | `src/app/icon.svg`, `src/app/favicon.ico`, `src/app/apple-icon.png`, `public/icon-192.png`, `public/icon-512.png` | générés depuis le logo par `scripts/generate-icons.mts` | idem logo | dérivés du logo, à régénérer si le logo change |
8 +| Image Open Graph | `src/app/opengraph-image.tsx` | générée au build (texte + spirale) | — | OK |
9 +| Photo du hero (bureau, plante, tasse, ordinateur) | `src/components/graphics/Placeholders.tsx` (`DeskScene`) | **illustration vectorielle de remplacement** créée pour le projet ; la photo de la maquette n'a pas été reprise (droits inconnus) | créée pour le projet | ⚠️ placeholder — fournir une photo sous licence |
10 +| Photo « À propos » (vase, feuillage, livres) | `src/components/graphics/Placeholders.tsx` (`VaseScene`) | idem | créée pour le projet | ⚠️ placeholder — fournir une photo sous licence |
11 +| Motifs (vagues, feuillage, spirales décoratives) | `src/components/graphics/*` | créés pour le projet | — | OK |
12 +| Icônes de services et d'interface | `src/components/ui/ServiceIcon.tsx`, `Icons.tsx` | dessinées pour le projet (trait 1,5) | — | OK |
13 +| Police titres **EB Garamond** | `next/font/google` (auto-hébergée au build) | Georg Duffner / Octavio Pardo, Google Fonts | SIL Open Font License 1.1 | OK ; remplacer par la police officielle du client si elle existe |
14 +| Police corps **Figtree** | `next/font/google` | Erik Kennedy, Google Fonts | SIL Open Font License 1.1 | OK |
15 +| Témoignage « Marie-Claude Deschamps, Directrice générale » | `content/home.json` (`approved: false`) | texte de la maquette, **identité non vérifiée** | — | ⚠️ NON PUBLIÉ ; obtenir un avis réel et une autorisation écrite |
16 +| Textes marketing (hero, services, à propos, bandeau) | `content/*.json` | repris mot pour mot de la maquette | client | à confirmer par le client |
17 +| Textes juridiques (confidentialité, mentions légales) | `src/app/(site)/confidentialite/page.tsx`, `pages.json` | gabarit rédigé pour le projet, décrit les pratiques réelles du site | — | ⚠️ à faire valider (`privacy.reviewed=false`) |
18 +| Coordonnées, LinkedIn | `content/site.json` | **vides** (non fournies) | — | à compléter par le client |
added docs/CLIENT_GUIDE.md +47 −0
@@ -0,0 +1,47 @@
1 +# Guide de gestion des contenus — PHI Conseil
2 +
3 +Tous les textes, services, avantages, témoignages, coordonnées et paramètres SEO se trouvent dans trois fichiers du dossier `content/`. Ils se modifient avec n'importe quel éditeur de texte (ou l'interface web du dépôt Git) ; aucune modification du code n'est nécessaire. À chaque mise à jour, le site est reconstruit et vérifié : une faute de structure (guillemet manquant, champ oublié) bloque la publication avec un message précis au lieu de casser le site.
4 +
5 +> Règle d'or : conservez les guillemets `"…"` et les virgules ; modifiez uniquement ce qui se trouve entre les guillemets, ou ajoutez/retirez des éléments complets dans les listes.
6 +
7 +## `content/site.json` — identité et coordonnées
8 +
9 +| Champ | Usage |
10 +| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
11 +| `brand.name`, `brand.tagline`, `brand.footerSignature`, `brand.legalName` | nom, signature de l'en-tête, phrase du pied de page, raison sociale du copyright |
12 +| `navigation[]` | liens du menu (ordre = ordre affiché) |
13 +| `headerCta` | bouton « Contactez-nous → » |
14 +| `contact.email`, `contact.phone`, `contact.address`, `contact.region` | coordonnées ; **laissez `""` pour masquer** l'icône courriel / la ligne correspondante |
15 +| `social.linkedin` | URL complète du profil LinkedIn ; `""` = icône masquée |
16 +| `legalLinks[]` | liens juridiques du pied de page |
17 +| `seo.siteUrl`, `seo.defaultTitle`, `seo.titleTemplate`, `seo.defaultDescription` | métadonnées par défaut |
18 +
19 +## `content/home.json` — page d'accueil
20 +
21 +- `hero.titleLines` : les lignes du grand titre (1 à 4). `hero.subtitle`, `hero.cta`.
22 +- `hero.image.src` : chemin d'une photo placée dans `public/images/` (ex. `"/images/hero.jpg"`). Vide = illustration vectorielle de remplacement. Toujours renseigner `alt` (description pour les lecteurs d'écran).
23 +- `services.items[]` : `slug` (identifiant URL, lettres minuscules et tirets), `icon` (`search`, `document-edit`, `chart`, `check-circle`), `title`, `summary` (accueil), `description` (page Services, facultatif). L'ordre du tableau = ordre d'affichage.
24 +- `about` : `overtitle`, `title`, `paragraph`, `cta`, `benefits[]` (coches dorées), `image`.
25 +- `testimonials.visible` : `true` pour afficher la section. **Un témoignage n'apparaît que si `approved: true`** (autorisation écrite de la personne citée). `previewOnly` permet d'afficher les exemples non approuvés uniquement sur un site de démonstration.
26 +- `callToAction` : bandeau bleu final.
27 +- Chaque section possède `visible` pour la masquer sans la supprimer.
28 +
29 +## `content/pages.json` — pages internes
30 +
31 +Titres, introductions, descriptions SEO des pages Services, À propos, Témoignages, Nous joindre (liste `subjects` du menu déroulant, texte de consentement, message de confirmation) et Confidentialité (`lastUpdated` au format AAAA-MM-JJ ; passez `reviewed` à `true` une fois le texte validé juridiquement pour retirer l'avertissement).
32 +
33 +## Images
34 +
35 +1. Préparer un JPEG/WebP de bonne qualité (hero : ≥ 1600 px de large ; À propos : format vertical ≥ 1000 × 1250 px).
36 +2. Le déposer dans `public/images/`.
37 +3. Renseigner `image.src` et `image.alt` dans `home.json`.
38 +4. Noter la provenance et la licence dans `docs/ASSET_REGISTER.md`.
39 + Le site convertit et redimensionne automatiquement (AVIF/WebP, tailles adaptées).
40 +
41 +## Logo et favicon
42 +
43 +Le logo actuel est une recréation SVG de la spirale dorée. Pour utiliser le logo officiel : remplacer le composant `src/components/graphics/Logo.tsx` par le SVG fourni, puis `pnpm icons` pour régénérer favicon et icônes (ou fournir directement `src/app/icon.svg`, `src/app/apple-icon.png`).
44 +
45 +## Publier une modification
46 +
47 +Sur le serveur d'hébergement : `pnpm build && pm2 restart phi-conseil` (voir `docs/DEPLOYMENT.md`), puis `git commit` pour conserver l'historique. Une erreur de contenu s'affiche au build sous la forme `Contenu invalide dans content/home.json : hero.subtitle : …`.
added docs/DEPLOYMENT.md +68 −0
@@ -0,0 +1,68 @@
1 +# Déploiement — PHI Conseil
2 +
3 +## Cible
4 +
5 +Application Node.js (`next start`) derrière un reverse proxy TLS. Aucune base de données. Le build est statique pour toutes les pages ; seule `POST /api/contact` s'exécute à la demande.
6 +
7 +| Besoin | Valeur |
8 +| -------- | ---------------------------------------------------------------- |
9 +| Node.js | ≥ 20 (testé 26) |
10 +| RAM | ~150 Mo au repos |
11 +| Port | `PORT` (défaut 3000) |
12 +| Fichiers | `.next/`, `public/`, `content/`, `node_modules/`, `package.json` |
13 +
14 +## Variables d'environnement
15 +
16 +Voir `.env.example`. En production :
17 +
18 +- `NEXT_PUBLIC_SITE_URL` — URL publique (canonical, sitemap, OG). **Inlinée au build** : rebuild si elle change.
19 +- `NEXT_PUBLIC_CONTENT_PREVIEW` — `0` en production. `1` affiche les témoignages de maquette avec un bandeau « exemple non vérifié » (sites de démonstration uniquement ; inlinée au build).
20 +- `CONTACT_TRANSPORT=smtp` + `CONTACT_TO`, `CONTACT_FROM`, `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASS` — sans ces valeurs le formulaire fonctionne mais **n'envoie aucun courriel** (`CONTACT_TRANSPORT=log`) et l'utilisateur en est informé dans l'écran de confirmation.
21 +- `CONTACT_RATE_LIMIT` / `CONTACT_RATE_WINDOW_S` — anti-abus (défaut 5 / 600 s par IP). Le limiteur est en mémoire : valable pour une instance unique (PM2 `fork`). Pour plusieurs instances, brancher Redis dans `src/lib/security.ts`.
22 +
23 +Le proxy doit transmettre `X-Forwarded-For` (Caddy et nginx le font par défaut) pour que la limitation par IP soit correcte.
24 +
25 +## Procédure générique (VPS / serveur Node)
26 +
27 +```bash
28 +git clone <dépôt> phi-conseil && cd phi-conseil
29 +pnpm install --frozen-lockfile
30 +cp .env.example .env.local && $EDITOR .env.local
31 +pnpm build
32 +pm2 start pnpm --name phi-conseil -- start -p 3000 # ou systemd
33 +```
34 +
35 +Reverse proxy (exemple Caddy) : `phiconseil.example { reverse_proxy 127.0.0.1:3000 }` — Caddy obtient et renouvelle le certificat TLS automatiquement. HSTS est déjà émis par l'application.
36 +
37 +### Vercel (alternative)
38 +
39 +Importer le dépôt, définir les variables d'environnement, framework Next.js détecté automatiquement. Le limiteur en mémoire n'est pas fiable en serverless : remplacer par Upstash/Redis ou le middleware de limitation du fournisseur.
40 +
41 +## Procédure MacLustr (`mld`) — site de démonstration test.spboucher.ai
42 +
43 +1. Manifeste `M1M32:~/dispatch/apps/phi-conseil.json` (dir `~/apps/phi-conseil`, PM2 `pnpm start -p 8520`, `tunnel.domain = test.spboucher.ai`, hooks `post_sync` : `pnpm install --frozen-lockfile && pnpm build`).
44 +2. `mld stage ~/Desktop/phi-conseil phi-conseil` puis `mld deploy phi-conseil`.
45 +3. DNS GoDaddy `A test.spboucher.ai → 51.161.112.61` (passerelle BHS64) — déjà en place ; `mld` pose la route Caddy et vérifie la santé publique.
46 +4. Mises à jour : modifier **sur le nœud** (clone git), `pnpm build && pm2 restart phi-conseil`, puis `git commit/push` depuis le nœud (règle remote-first MacLustr).
47 +
48 +## Sauvegardes et restauration
49 +
50 +- Le code et le contenu (`content/*.json`) sont versionnés dans Git : la sauvegarde est le dépôt distant. Restaurer = `git checkout <tag>` + `pnpm install && pnpm build`.
51 +- Les messages du formulaire ne sont pas stockés par le site : ils vivent dans la boîte courriel destinataire (sauvegarde relevant du fournisseur de messagerie).
52 +- Secrets (`.env.local`) : conserver une copie chiffrée hors du dépôt (gestionnaire de mots de passe).
53 +
54 +## Retour arrière
55 +
56 +1. `git log` → identifier la dernière version saine (chaque mise en production est taguée `vX.Y.Z`).
57 +2. `git checkout vX.Y.Z && pnpm install --frozen-lockfile && pnpm build && pm2 restart phi-conseil`.
58 +3. Vérifier `GET /` (200) et `GET /api/contact` (405 attendu) ; sur MacLustr, `mld status --live phi-conseil`.
59 +
60 +## Surveillance minimale
61 +
62 +- Santé : `GET /` doit répondre 200 (healthcheck PM2/mld).
63 +- Journaux : `pm2 logs phi-conseil` — les lignes `[contact]` ne contiennent qu'une empreinte d'IP, jamais le contenu des messages.
64 +- Erreurs de rendu : digest côté serveur (`console.error`) corrélable à l'écran d'erreur client. Un service d'erreurs (Sentry…) peut être ajouté dans `src/app/error.tsx` et `instrumentation.ts` ; non inclus par défaut pour éviter tout traceur non consenti.
65 +
66 +## Mise à jour des dépendances
67 +
68 +`pnpm outdated` puis `pnpm update --latest` en suivant les notes de migration de Next.js ; relancer `pnpm check` et `pnpm test:e2e` avant mise en production. Audit : `pnpm audit`.
added docs/IMPLEMENTATION_PLAN.md +87 −0
@@ -0,0 +1,87 @@
1 +# Plan d'implémentation — PHI Conseil
2 +
3 +Date : 2026-11-08. Référence visuelle : `references/phi-conseil-homepage.jpeg` (1024 × 1536 px, capture desktop fournie par le client).
4 +
5 +## 1. Audit initial
6 +
7 +| Élément | État constaté |
8 +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9 +| Dépôt | Vide à l'origine (créé aujourd'hui). Scaffold `create-next-app` 16.4.5, TypeScript strict, Tailwind 4, ESLint 9, pnpm 11. |
10 +| Capture | Présente et inspectée. Couleurs échantillonnées par région (médiane / mode / moyenne des pixels sombres) — voir `src/styles/tokens.css`. |
11 +| Actifs client | **Aucun** fichier original fourni : pas de logo vectoriel, pas de photos sous licence, pas de polices, pas de coordonnées, pas de textes juridiques, pas de témoignage autorisé. |
12 +| Hébergement | Non précisé. Cible par défaut : Node 20+ (`next start` derrière un reverse proxy TLS). Un déploiement sur le cluster MacLustr (`mld`) ou sur Vercel est documenté dans `docs/DEPLOYMENT.md`. |
13 +
14 +### Inconnues bloquantes (à valider par le client)
15 +
16 +1. Logo vectoriel officiel (sinon le SVG recréé `src/components/graphics/Logo.tsx` doit être approuvé).
17 +2. Photos sous licence pour le hero et la section À propos (les illustrations livrées sont des **placeholders vectoriels** dans la palette, marqués comme tels dans `docs/ASSET_REGISTER.md`).
18 +3. Coordonnées réelles (courriel, téléphone, adresse, LinkedIn) : laissées vides, les icônes/liens correspondants sont masqués tant que les champs du contenu sont vides.
19 +4. Témoignage(s) autorisé(s) : la section est **masquée en production** (`testimonials.visible=false`) ; le texte de la maquette est conservé uniquement comme exemple de prévisualisation marqué « non vérifié ».
20 +5. Textes juridiques (confidentialité, mentions légales) : gabarit fourni, à faire valider par un conseiller juridique.
21 +6. Fournisseur de messagerie transactionnelle (SMTP) et adresses expéditeur/destinataire.
22 +7. Choix CMS : voir §4.
23 +
24 +## 2. Architecture
25 +
26 +```
27 +content/ ← contenu administrable (JSON validé par Zod au build et à l'exécution)
28 + site.json ← identité, navigation, coordonnées, liens sociaux, SEO par défaut
29 + home.json ← hero, services, à-propos, témoignages, bandeau, visibilité des sections
30 + pages.json ← pages internes (services, à propos, témoignages, nous joindre, confidentialité)
31 +src/
32 + app/
33 + layout.tsx ← polices, métadonnées par défaut, Header/Footer
34 + (site)/page.tsx ← accueil (7 sections)
35 + (site)/services|a-propos|temoignages|nous-joindre|confidentialite/page.tsx
36 + api/contact/route.ts ← POST : validation Zod, honeypot, limitation de débit, envoi SMTP
37 + sitemap.ts, robots.ts, icon.svg, opengraph-image.tsx
38 + components/
39 + layout/ Header, MobileNav, Footer
40 + sections/ Hero, Services, About, Testimonials, CallToAction
41 + graphics/ Logo, GoldenSpiral, WaveDivider, BotanicalMotif, placeholders photo
42 + ui/ Button, Container, SectionTitle, Icon…
43 + forms/ ContactForm (client)
44 + lib/ content.ts (chargeur + schémas), env.ts, validation.ts, security.ts (rate limit, sanitisation), mail.ts, seo.ts
45 + styles/ tokens.css (design tokens), globals.css
46 +tests/
47 + unit/ Vitest + Testing Library (schémas, validation, rate limit, composants)
48 + e2e/ Playwright (navigation, menu mobile, formulaire, liens, axe, captures)
49 +docs/ IMPLEMENTATION_PLAN, DEPLOYMENT, CLIENT_GUIDE, QA_REPORT, ASSET_REGISTER
50 +```
51 +
52 +## 3. Design system
53 +
54 +- Tokens CSS (`src/styles/tokens.css`) : couleurs échantillonnées, échelle typographique `clamp()`, espacements, rayons, ombres, mouvement. Exposés à Tailwind 4 via `@theme`.
55 +- Titres : **EB Garamond** (OFL) ; corps : **Figtree** (OFL). Chargées par `next/font/google` (auto-hébergées au build, `display: swap`). À remplacer par les polices officielles du client si elles existent.
56 +- Motifs : spirale de Fibonacci (carrés + arcs de quart de cercle, ratio 1,618 approché par la suite 1-1-2-3-5-8-13-21), vagues, feuillage — tous en SVG inline.
57 +
58 +## 4. Contenu administrable — décision
59 +
60 +Le cahier des charges recommande un CMS maintenu (Payload / Sanity) tout en demandant d'éviter de surdimensionner si une solution statique convient et de **demander validation**.
61 +
62 +Choix livré : **contenu structuré en fichiers JSON versionnés** (`content/*.json`), validé par Zod, modifiable sans toucher au code applicatif, avec guide client. Raisons : aucun hébergement de base de données ni de compte tiers à gérer, pas de surface d'authentification à sécuriser, sauvegarde = historique Git, coût nul, déploiement statique possible.
63 +
64 +Compromis documentés : l'édition exige un éditeur de texte ou l'interface web Git ; pas d'aperçu WYSIWYG ni de gestion de médias par glisser-déposer. Chemin d'évolution : `src/lib/content.ts` est l'unique point d'accès au contenu ; brancher Payload (SQLite/Postgres, intégré à Next) ou Sanity consiste à remplacer ce module en conservant les schémas Zod. **Décision à valider par le client.**
65 +
66 +## 5. Étapes
67 +
68 +1. Audit + plan (ce document). ✔
69 +2. Design system : tokens, polices, UI de base, logo et motifs SVG.
70 +3. Accueil fidèle : 7 sections, comparaison Playwright à 1024 px (largeur de la capture) puis 1440 px.
71 +4. Pages internes + chargeur de contenu + visibilité des sections.
72 +5. Formulaire de contact + API + messagerie SMTP configurable + protection antispam.
73 +6. Validation : lint, typecheck, Vitest, Playwright (6 largeurs, axe), build de production.
74 +7. Livraison : README, .env.example, docs/DEPLOYMENT, CLIENT_GUIDE, QA_REPORT, ASSET_REGISTER.
75 +
76 +## 6. Critères d'acceptation par section (checklist)
77 +
78 +- [ ] En-tête : logo + nom + signature, 5 liens, CTA bleu arrondi « Contactez-nous → », lien actif souligné, menu mobile accessible, en-tête collant sobre.
79 +- [ ] Hero : fond ivoire, H1 serif 3 lignes, sous-titre, bouton bleu, illustration à droite avec spirale dorée superposée, vagues ivoire/bleu pâle en bas.
80 +- [ ] Services : titre centré entre filets dorés, 4 cartes (disque bleu-gris + icône au trait), textes exacts.
81 +- [ ] À propos : image verticale à gauche avec courbe ivoire, surtitre doré, H2, paragraphe, bouton contour, 4 bénéfices avec coches dorées séparés par un filet vertical.
82 +- [ ] Témoignages : encadré bleu pâle arrondi, guillemets dorés, serif italique, pagination ; slider accessible ; masqué en prod sans avis validé.
83 +- [ ] Bandeau : fond bleu ardoise, titre blanc, bouton ivoire, spirale + feuillage dorés à droite.
84 +- [ ] Pied de page : logo + nom, liens, icônes LinkedIn/courriel (si fournis), copyright dynamique, signature, liens légaux.
85 +- [ ] Responsive 360/390/768/1024/1440/1920 sans débordement horizontal.
86 +- [ ] Formulaire : validation client/serveur, honeypot, rate limit, SMTP configurable, états succès/erreur, tests.
87 +- [ ] Lint, typecheck, tests, build, axe : verts ou écarts documentés dans `docs/QA_REPORT.md`.
added docs/screenshots/.gitkeep +0 −0
modified next.config.ts +9 −1
@@ -1,9 +1,17 @@
1 1 import type { NextConfig } from "next";
2 +import { securityHeaders } from "./src/lib/security";
2 3
3 4 const nextConfig: NextConfig = {
4 − /* config options here */
5 5 cacheComponents: true,
6 6 partialPrefetching: true,
7 + poweredByHeader: false,
8 + reactStrictMode: true,
9 + images: {
10 + formats: ["image/avif", "image/webp"],
11 + },
12 + async headers() {
13 + return [{ source: "/(.*)", headers: securityHeaders }];
14 + },
7 15 turbopack: {
8 16 rules: {
9 17 "*.css": {
modified package.json +27 −3
@@ -6,22 +6,46 @@
6 6 "dev": "next dev",
7 7 "build": "next build",
8 8 "start": "next start",
9 − "lint": "eslint"
9 + "lint": "eslint",
10 + "typecheck": "tsc --noEmit",
11 + "format": "prettier --write .",
12 + "format:check": "prettier --check .",
13 + "test": "vitest run",
14 + "test:watch": "vitest",
15 + "test:e2e": "playwright test",
16 + "icons": "node --experimental-strip-types scripts/generate-icons.mts",
17 + "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm build"
10 18 },
11 19 "dependencies": {
20 + "@hookform/resolvers": "^7.0.3",
12 21 "next": "16.4.5",
22 + "nodemailer": "^10.3.1",
13 23 "react": "19.3.0",
14 − "react-dom": "19.3.0"
24 + "react-dom": "19.3.0",
25 + "react-hook-form": "^7.91.0",
26 + "sharp": "^0.35.5",
27 + "zod": "^4.6.7"
15 28 },
16 29 "devDependencies": {
30 + "@axe-core/playwright": "^4.14.0",
31 + "@playwright/test": "^1.65.0",
17 32 "@tailwindcss/turbopack": "^4",
33 + "@testing-library/jest-dom": "^7.0.1",
34 + "@testing-library/react": "^16.3.3",
35 + "@testing-library/user-event": "^14.6.8",
18 36 "@types/node": "^20",
37 + "@types/nodemailer": "^8.0.2",
19 38 "@types/react": "^19",
20 39 "@types/react-dom": "^19",
40 + "@vitejs/plugin-react": "^6.2.1",
21 41 "eslint": "^9",
22 42 "eslint-config-next": "16.4.5",
43 + "jsdom": "^31.0.0",
44 + "prettier": "^3.9.10",
45 + "prettier-plugin-tailwindcss": "^0.8.1",
23 46 "tailwindcss": "^4",
24 − "typescript": "^5"
47 + "typescript": "^5",
48 + "vitest": "^5.1.0"
25 49 },
26 50 "packageManager": "pnpm@11.1.2"
27 51 }
added playwright.config.ts +26 −0
@@ -0,0 +1,26 @@
1 +import { defineConfig, devices } from "@playwright/test";
2 +
3 +const PORT = process.env.PW_PORT ?? "3100";
4 +const baseURL = `http://127.0.0.1:${PORT}`;
5 +
6 +export default defineConfig({
7 + testDir: "./tests/e2e",
8 + fullyParallel: true,
9 + retries: process.env.CI ? 1 : 0,
10 + reporter: process.env.CI ? [["list"], ["html", { open: "never" }]] : "list",
11 + use: { baseURL, trace: "retain-on-failure", locale: "fr-CA" },
12 + webServer: {
13 + command: `pnpm start -p ${PORT}`,
14 + url: baseURL,
15 + reuseExistingServer: !process.env.CI,
16 + timeout: 120_000,
17 + env: { CONTACT_TRANSPORT: "log", NEXT_PUBLIC_SITE_URL: baseURL },
18 + },
19 + projects: [
20 + {
21 + name: "desktop",
22 + use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 900 } },
23 + },
24 + { name: "mobile", use: { ...devices["iPhone 13"] } },
25 + ],
26 +});
modified pnpm-lock.yaml +1098 −5
Large diff (1,740 lines) is not loaded by default to keep the page fast.Load diff
deleted public/file.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
deleted public/globe.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added public/icon-192.png +0 −0

Binary file not shown.

added public/icon-512.png +0 −0

Binary file not shown.

added public/logo-mark.svg +4 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
deleted public/next.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
deleted public/vercel.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
deleted public/window.svg +0 −1
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added references/phi-conseil-homepage.jpeg +0 −0

Binary file not shown.

added scripts/generate-icons.mts +58 −0
Large diff (58 lines) is not loaded by default to keep the page fast.Load diff
added scripts/overflow.mjs +18 −0
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
added scripts/shot.mjs +9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/a-propos/page.tsx +65 −0
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/confidentialite/page.tsx +99 −0
Large diff (99 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/nous-joindre/page.tsx +77 −0
Large diff (77 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/page.tsx +22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/services/page.tsx +52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added src/app/(site)/temoignages/page.tsx +50 −0
Large diff (50 lines) is not loaded by default to keep the page fast.Load diff
added src/app/api/contact/route.ts +96 −0
Large diff (96 lines) is not loaded by default to keep the page fast.Load diff
added src/app/apple-icon.png +0 −0

Binary file not shown.

added src/app/error.tsx +35 −0
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/favicon.ico +0 −0

Binary file not shown.

modified src/app/globals.css +153 −16
Large diff (179 lines) is not loaded by default to keep the page fast.Load diff
added src/app/icon.svg +4 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
modified src/app/layout.tsx +48 −16
Large diff (77 lines) is not loaded by default to keep the page fast.Load diff
added src/app/manifest.ts +18 −0
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
added src/app/not-found.tsx +21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added src/app/opengraph-image.tsx +65 −0
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
deleted src/app/page.tsx +0 −69
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added src/app/robots.ts +9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
added src/app/sitemap.ts +19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added src/components/forms/ContactForm.tsx +275 −0
Large diff (275 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/BotanicalMotif.tsx +34 −0
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/GoldenSpiral.tsx +66 −0
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/Logo.tsx +46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/Placeholders.tsx +168 −0
Large diff (168 lines) is not loaded by default to keep the page fast.Load diff
added src/components/graphics/WaveDivider.tsx +28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added src/components/layout/Footer.tsx +102 −0
Large diff (102 lines) is not loaded by default to keep the page fast.Load diff
added src/components/layout/Header.tsx +30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added src/components/layout/MobileNav.tsx +114 −0
Large diff (114 lines) is not loaded by default to keep the page fast.Load diff
added src/components/layout/NavLinks.tsx +47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/About.tsx +78 −0
Large diff (78 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/CallToAction.tsx +46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/Hero.tsx +55 −0
Large diff (55 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/Services.tsx +34 −0
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/TestimonialSlider.tsx +106 −0
Large diff (106 lines) is not loaded by default to keep the page fast.Load diff
added src/components/sections/Testimonials.tsx +27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added src/components/ui/Button.tsx +88 −0
Large diff (88 lines) is not loaded by default to keep the page fast.Load diff
added src/components/ui/Icons.tsx +63 −0
Large diff (63 lines) is not loaded by default to keep the page fast.Load diff
added src/components/ui/SectionTitle.tsx +22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added src/components/ui/ServiceIcon.tsx +57 −0
Large diff (57 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/content.ts +184 −0
Large diff (184 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/env.ts +62 −0
Large diff (62 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/mail.ts +85 −0
Large diff (85 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/security.ts +93 −0
Large diff (93 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/seo.ts +39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/spiral.ts +81 −0
Large diff (81 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/time.ts +8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added src/lib/validation.ts +64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added src/styles/tokens.css +100 −0
Large diff (100 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/a11y.spec.ts +25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/contact.spec.ts +40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/mobile-menu.spec.ts +37 −0
Large diff (37 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/navigation.spec.ts +85 −0
Large diff (85 lines) is not loaded by default to keep the page fast.Load diff
added tests/e2e/screenshots.spec.ts +22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/components.test.tsx +43 −0
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/contact-route.test.ts +108 −0
Large diff (108 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/content.test.ts +39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/security.test.ts +51 −0
Large diff (51 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/setup.ts +20 −0
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
added tests/unit/validation.test.ts +47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
modified tsconfig.json +3 −2
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added vitest.config.mts +14 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme