Skip to content
Lantorian

Across the stack12 min read

Web best practices

Performance, accessibility, security, testing, SEO and code review: the common foundation of every modern web application we ship.

Performance

Google measures real experience with three Core Web Vitals. We aim for green on mobile, at our users' 75th percentile. Move the sliders to see thresholds and fixes.

3.1s
0good ≤ 2.5s6s

Needs improvement

Time to render the largest visible element (hero image, heading).

Render on the server, add priority to the hero image, set sizes, avoid blocking fonts.

180ms
0good ≤ 200ms800ms

Good

Delay between an interaction (click, keypress) and the visible screen update.

0.18
0good ≤ 0.10.4

Needs improvement

Sum of unexpected layout shifts during the visit.

Dimensions on images, correctly sized skeletons, next/font, no banner injected above content.

Official thresholds: green up to the first limit, orange up to the second, red beyond.
  • Server Components by default: the fastest JavaScript is the JavaScript you don't send.
  • next/image for every image: modern formats, right sizes, lazy loading.
  • next/font for fonts: hosted with the app, no layout shift.
  • Dynamic imports for heavy libraries (charts, editors) used below the fold.
  • Measure before optimizing: next experimental-analyze for the bundle, Vercel Speed Insights for real visits.
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
/>

Accessibility

Target: WCAG 2.2 level AA. It's also a legal requirement for many services in Europe since June 2025 (European Accessibility Act).

  • Semantic HTML first: button for actions, a for navigation, headings in order.
  • Every field has a visible label, and every error is linked to its field with aria-describedby.
  • Everything by keyboard: Tab through it, focus always visible, no traps in dialogs.
  • Contrast of at least 4.5:1 for text, and information never relies on color alone.
  • Changing content (loading, toasts) announced with aria-live or role="status".
  • Reduced motion respected: animations disabled with prefers-reduced-motion.

Security

A Server Action is a public HTTP endpoint: anyone can call it with any arguments. It validates its input and lets Laravel check permissions.

Avoid

actions.ts
'use server';

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

Do

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');
}
  • No secrets client-side: no API key in a Client Component or in a NEXT_PUBLIC_ variable.
  • server-only imported in every file that reads cookies, secrets or calls the API.
  • No dangerouslySetInnerHTML with user content; otherwise sanitize with DOMPurify.
  • Security headers configured in next.config.ts, CSP on publicly exposed projects.
  • Up-to-date dependencies: Dependabot on, npm audit and composer audit in 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=()' },
      ],
    },
  ];
}

Front-end testing

Vitest and Testing Library for components (test what users see, by role and text), Playwright for full journeys and automated accessibility with axe, in both languages.

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 and metadata

For public pages: generateMetadata translated per language, hreflang tags through alternates, sitemap.ts and robots.ts. Internal apps add 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}`,
      },
    },
  }));
}

Code review

When you open a PR

  • Review your own PR in the GitHub UI before assigning anyone: half the comments disappear.
  • Explain the why in the description, not just the what. Add FR and EN screenshots when the UI changes.
  • Reply to every comment, even with "fixed in abc123".

When you review

  • Comment on the code, not the person, and suggest a fix when you raise a problem.
  • Prefix the weight: blocking:, suggestion:, question:.
  • Reply within one business day: a waiting PR blocks a teammate.