Aller au contenu
mouaad.sek:~$
Tous les dossiers~/work/cf-01$
CF-01Produit · En ligneSTATUS: EN PRODUCTION

Travistello

Une plateforme d’aide à la décision où chaque note est calculée par un modèle versionné à partir de sources citées, et où les règles de confiance sont dans le code, pas dans un avertissement.

RÔLE
Seul, de la conception à la production
QUAND
2026
STACK
Next.js 16, React 19, TypeScript, Payload 3, Neon Postgres, Three.js, Vercel, Vitest, Playwright
01 / CONTEXTE

Le problème

La plupart des sites d’avis cachent comment leurs notes sont construites, mélangent « testé » et « lu quelque part », et redirigent les clics via des liens que n’importe qui peut détourner.

Travistello fait le pari inverse : un catalogue où chaque produit, variante, caractéristique, source et note est un enregistrement typé, et où la plateforme refuse de publier ce qu’elle ne peut pas prouver.

02 / APPROCHE

De la source brute au clic

  1. 01Produit + variante
  2. 02Sources
  3. 03Note versionnée
  4. 04Offre
  5. 05Comparatif
  6. 06Redirection tracée
03 / RÈGLES DE CONFIANCE ET DE SÉCURITÉ DANS LE CODE

Règles de confiance et de sécurité dans le code

RÈGLECOUCHEEMPÊCHE
Redirections /go/[token] opaques, destinations gérées côté serveurRoutageOpen redirect
Aucune adresse IP stockée sur les clics sortants, bots filtrésAnalyticsAbus de tracking
Les poids doivent faire 100 %, la note globale est calculéeModèle de donnéesNotes arrangées
Avis sur recherche et avis testés sont deux états distinctsModèle de donnéesFaux avis
Chaque média exige un ayant droit et une base d’utilisationCMSProblèmes de droits
Programmes d’affiliation et clics réservés aux adminsRôles CMSFuite de données
04 / PREUVES

Directement depuis le code

Extraits du vrai dépôt et des livrables, raccourcis pour la lecture.

src/lib/outbound.tstypescript
export const getSafeOutboundURL = (value: null | string | undefined) => {
  if (!value) return null;

  try {
    const url = new URL(value);

    if (
      !["http:", "https:"].includes(url.protocol) ||
      url.username ||
      url.password
    )
      return null;

    return url;
  } catch {
    return null;
  }
};

Chaque lien sortant passe par ce contrôle : uniquement http(s), jamais d’identifiants intégrés. Les destinations viennent de la base, jamais de l’URL.

src/app/go/[token]/route.tstypescript
export async function GET(request: NextRequest, { params }: RouteContext) {
  const { token } = await params;
  const offers = await payload.find({
    collection: "offers",
    limit: 1,
    where: {
      and: [
        { publicToken: { equals: token } },
        { status: { equals: "active" } },
      ],
    },
  });
  const offer = offers.docs[0];

  if (!offer || !isPublicOffer(offer))
    return new NextResponse("Offer not found.", { status: 404 });

  const destination = getSafeOutboundURL(offer.affiliateUrl || offer.destinationUrl);
  if (!destination)
    return new NextResponse("Offer destination is unavailable.", { status: 503 });

  const bot = isLikelyBot(request.headers.get("user-agent"));
  // ... only non-bot clicks are recorded, with no IP address
}

La redirection /go/[token] : le token est résolu côté serveur, les offres inactives renvoient 404, et les bots ne créent jamais de clic.

05 / RÉSULTATS

Résultats

28produits publiés dans 5 catégories
6comparatifs
3lots livrés, en ligne et vérifiés
06 / RETOUR D’EXPÉRIENCE

Ce que je ferais autrement

Le plus dur n’a jamais été les fonctionnalités, mais les règles de confiance : décider ce que la plateforme doit refuser d’afficher. Prochaine étape : activer les programmes d’affiliation et ajouter les premiers tests réels comme état de données à part.

DOSSIER SUIVANT · CF-02Sécurité de la supply chain sur Kubernetes
Esc

↑↓ naviguer · Entrée ouvrir · Échap fermer