Aller au contenu
Lantorian

Transverse12 min de lecture

Bonnes pratiques web

Performance, accessibilité, sécurité, tests, SEO et revue de code : le socle commun à toute application web moderne que l'on livre.

Performance

Google mesure l'expérience réelle avec trois Core Web Vitals. On vise le vert sur mobile, au 75e centile de nos utilisateurs. Déplace les curseurs pour voir les seuils et les corrections.

3.1s
0bon ≤ 2.5s6s

À améliorer

Temps d'affichage du plus grand élément visible (image principale, titre).

Rends la page côté serveur, ajoute priority sur l'image principale, précise sizes, évite les polices bloquantes.

180ms
0bon ≤ 200ms800ms

Bon

Délai entre une interaction (clic, frappe) et la mise à jour visible de l'écran.

0.18
0bon ≤ 0.10.4

À améliorer

Somme des sauts de mise en page inattendus pendant la visite.

Dimensions sur les images, skeletons à la bonne taille, next/font, pas de bannière injectée au-dessus du contenu.

Seuils officiels : vert jusqu'à la première limite, orange jusqu'à la seconde, rouge au-delà.
  • Server Components par défaut : le JavaScript le plus rapide est celui qu'on n'envoie pas.
  • next/image pour toute image : formats modernes, tailles adaptées, chargement différé.
  • next/font pour les polices : hébergées avec l'app, sans saut de mise en page.
  • Import dynamique des bibliothèques lourdes (graphiques, éditeurs) utilisées sous la ligne de flottaison.
  • Mesurer avant d'optimiser : next experimental-analyze pour le bundle, Vercel Speed Insights pour les vraies visites.
product-card.tsx
import Image from 'next/image';

<Image
  src={product.imageUrl}
  alt={product.name}          // describes the image, never "image"
  width={640}
  height={480}
  sizes="(min-width: 1024px) 33vw, 100vw"
  priority={index === 0}      // only the LCP image above the fold
/>

Accessibilité

Cible : WCAG 2.2 niveau AA. C'est aussi une obligation légale pour de nombreux services en Europe depuis juin 2025 (European Accessibility Act).

  • HTML sémantique d'abord : button pour une action, a pour une navigation, titres dans l'ordre.
  • Chaque champ a un label visible, et chaque erreur est reliée au champ avec aria-describedby.
  • Tout au clavier : parcours avec Tab, focus toujours visible, pas de piège dans les modales.
  • Contraste de 4,5:1 minimum pour le texte, et l'information ne passe jamais par la couleur seule.
  • Contenu qui change (chargement, toasts) annoncé avec aria-live ou role="status".
  • Mouvement réduit respecté : animations coupées avec prefers-reduced-motion.

Sécurité

Une Server Action est un endpoint HTTP public : n'importe qui peut l'appeler avec n'importe quels arguments. Elle valide ses entrées et laisse Laravel vérifier les droits.

À éviter

actions.ts
'use server';

// Anyone can call this endpoint with any id
export async function deleteInvoice(id: number) {
  await db.invoice.delete({ where: { id } });
}

À faire

actions.ts
'use server';

export async function deleteInvoice(input: unknown) {
  const { id } = deleteInvoiceSchema.parse(input); // validate
  // Laravel checks the session token AND the InvoicePolicy
  await apiFetch(`/v1/invoices/${id}`, { method: 'DELETE' });
  updateTag('invoices');
}
  • Jamais de secret côté client : pas de clé d'API dans un Client Component ni dans une variable NEXT_PUBLIC_.
  • server-only importé dans tout fichier qui lit des cookies, des secrets ou appelle l'API.
  • Pas de dangerouslySetInnerHTML avec du contenu venant d'un utilisateur ; sinon, nettoyage avec DOMPurify.
  • En-têtes de sécurité configurés dans next.config.ts, CSP sur les projets exposés publiquement.
  • Dépendances à jour : Dependabot activé, npm audit et composer audit dans la CI.
next.config.ts
// next.config.ts
async headers() {
  return [
    {
      source: '/(.*)',
      headers: [
        { key: 'X-Content-Type-Options', value: 'nosniff' },
        { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
        { key: 'X-Frame-Options', value: 'DENY' },
        { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
      ],
    },
  ];
}

Tests front

Vitest et Testing Library pour les composants (on teste ce que voit l'utilisateur, par rôle et par texte), Playwright pour les parcours complets et l'accessibilité automatisée avec axe, dans les deux langues.

src/features/invoices/components/invoice-filters.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { NextIntlClientProvider } from 'next-intl';
import { describe, expect, it } from 'vitest';
import messages from '../../../../messages/fr/invoices.json';
import { InvoiceFilters } from './invoice-filters';

const renderFr = (ui: React.ReactElement) =>
  render(<NextIntlClientProvider locale="fr" messages={{ invoices: messages }}>{ui}</NextIntlClientProvider>);

describe('InvoiceFilters', () => {
  it('marks the selected status as pressed', async () => {
    renderFr(<InvoiceFilters />);

    await userEvent.click(screen.getByRole('button', { name: 'Payée' }));

    expect(screen.getByRole('button', { name: 'Payée' })).toHaveAttribute('aria-pressed', 'true');
  });
});

SEO et métadonnées

Pour les pages publiques : generateMetadata traduit par langue, balises hreflang via alternates, sitemap.ts et robots.ts. Les applications internes ajoutent robots: noindex.

src/app/sitemap.ts
import type { MetadataRoute } from 'next';

// app/sitemap.ts: one entry per page, with its translations
export default function sitemap(): MetadataRoute.Sitemap {
  return ['', '/pricing', '/contact'].map((path) => ({
    url: `https://app.example.com/fr${path}`,
    alternates: {
      languages: {
        fr: `https://app.example.com/fr${path}`,
        en: `https://app.example.com/en${path}`,
      },
    },
  }));
}

Revue de code

Quand tu ouvres une PR

  • Relis ta propre PR dans l'interface GitHub avant d'assigner quelqu'un : la moitié des remarques disparaissent.
  • Explique le pourquoi dans la description, pas seulement le quoi. Ajoute des captures FR et EN si l'interface change.
  • Réponds à chaque commentaire, même par « corrigé dans abc123 ».

Quand tu relis

  • Commente le code, pas la personne, et propose une solution quand tu signales un problème.
  • Préfixe l'importance : bloquant:, suggestion:, question:.
  • Réponds sous 24 heures ouvrées : une PR qui attend bloque un collègue.