Files
PleinR/CLAUDE.md
T
Claude a8adb9ae6a Référencement : métadonnées complètes, données structurées, sitemap et robots
- Layout : metadataBase, gabarit de titre « %s · Plein R », description,
  mots-clés, Open Graph / Twitter, robots, icônes, manifeste, viewport et
  couleur de thème ; JSON-LD Organization + WebSite (recherche annuaire).
- Chaque page publique pose titre, description (≤ 160 caractères) et
  canonique via pageMetadata() ; l'annuaire ignore ainsi ?q=. Fiche adhérent :
  titre « Nom · Catégorie à Ville », description dérivée, LocalBusiness avec
  adresse, contacts et horaires (OpeningHoursSpecification), BreadcrumbList.
  Annuaire : ItemList des adhérents. Association : BusinessEvent par
  rencontre à venir. Rencontres passées : fil d'Ariane.
- Nouvelles routes : robots.txt, sitemap.xml (pages statiques + adhérents
  actifs), manifest.webmanifest, image de partage 1200×630 générée avec le
  logo, page 404 dédiée (noindex).
- /backend, /login, /inscription/* et les fiches inactives en noindex.
- Landmarks <main> sur toutes les pages publiques, fil d'Ariane en <nav>.
- serializeJsonLd() échappe <, > et & : les saisies des adhérents ne peuvent
  pas fermer le bloc <script>. Tests unitaires dans tests/seo.test.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-05 10:41:53 +00:00

7.9 KiB
Raw Blame History

CLAUDE.md

Guidance for working in this repository.

What this is

Plein R — website for an association of merchants/businesses in the Bassin de Pompey. A single Next.js application serving a public Accueil page and an authenticated, role-based Backend (admin + member space). Postgres is an external container.

Stack

  • Next.js 15 (App Router, TypeScript, output: "standalone")
  • PostgreSQL 16 + Drizzle ORM (src/db/schema.ts) with drizzle-kit migrations
  • Auth.js v5 (credentials provider, JWT sessions) — RBAC in src/lib/rbac.ts
  • One app container + one Postgres container (docker-compose.yml)

Commands

npm run dev            # dev server
npm run build          # production build
npm run db:generate    # regenerate SQL after editing src/db/schema.ts
npm run db:migrate     # apply migrations
npm run db:seed        # catégories + admin initial ; données de démo si SEED_DEMO=true
npm run db:purge-demo  # retire les données de démo d'une base qui les a reçues
docker compose up --build   # full stack

Conventions

  • Styling: faithful port of the original design as inline styles + a small design-system in src/app/globals.css (palette as CSS vars, fonts, twinkle/float animations, hover lifts, responsive grid helpers). No Tailwind.
  • Mutations: server actions in src/app/backend/actions.ts. Each action re-checks auth + capability via getSession() and can() before writing, then revalidatePath().
  • Access control: src/middleware.ts gates /backend/*; each page further guards by role (isStaff, can) and redirects.
  • Data reads for the public site live in src/lib/queries.ts.
  • After changing src/db/schema.ts, run npm run db:generate and commit the new file under drizzle/.

Promotions

Statuts : pending → live → suspended ⇄ live (+ rejected / expired). promotions.suspended_by retient qui a suspendu (member ou staff) : une suspension par le staff ne peut être levée que par le staff. Les lectures publiques filtrent sur status = 'live', donc une promo suspendue disparaît du site sans traitement supplémentaire.

Réseaux sociaux

  • src/lib/social.ts publie une promo sur la page Facebook (Graph API) ou LinkedIn (Posts API). src/lib/social-accounts.ts gère la configuration : OAuth, jetons, cibles. Réseau non configuré = case masquée.
  • Les identifiants et jetons vivent en base (social_accounts), chiffrés via src/lib/crypto.ts (AES-256-GCM, clé SOCIAL_TOKEN_KEY ou AUTH_SECRET), posés depuis /backend/reseaux. Les variables d'environnement restent lues en repli. Aucun secret ne doit jamais repartir vers le navigateur.
  • isNetworkConfigured() / configuredNetworks() / siteUrl() / redirectUri() sont asynchrones (accès base).
  • L'URL publique est le réglage site_public_url, édité sur /backend/reseaux (pré-rempli depuis les en-têtes de la requête). saveSiteSettings saute cette clé : le formulaire Paramètres ne la contient pas et l'écraserait.
  • Routes OAuth : src/app/api/social/[network]/{connect,callback}. Le state anti-CSRF passe par un cookie httpOnly ; aucun jeton ne transite par une URL.
  • Facebook : le jeton de page n'expire pas. LinkedIn : 60 jours, rafraîchissement programmatique réservé à certains partenaires, d'où le bandeau de reconnexion.
  • Les images de promo sont stockées en data-URI : l'upload se fait donc en binaire (multipart pour Facebook, Images API en 3 étapes pour LinkedIn), pas par URL.
  • La diffusion est déclenchée par la validation, pas par un bouton : promotions.share_facebook / share_linkedin sont choisis par l'adhérent, ajustables par le modérateur dans le formulaire « Valider », puis figés (status !== 'pending').
  • publishPromoShares() dans backend/actions.ts est le seul point de publication. Elle ne lève jamais et ignore tout réseau ayant déjà une ligne social_posts en posted : c'est la garde anti-republication, qui couvre aussi le cycle suspension → remise en ligne.
  • retryPromoShare ne sert qu'au rattrapage d'un échec sur un réseau déjà choisi ; il ne peut pas élargir la diffusion.
  • Les URLs publiques des pages FB/LinkedIn sont des site_settings (association_facebook, association_linkedin), éditables dans Paramètres.

Sécurité

  • Le journal d'activité agrège des saisies de tiers, dont le formulaire de contact public : il est filtré à l'écriture (sanitizeActivityMessage) et rendu en éléments React (activityNodes), jamais en HTML brut.
  • getSession() (src/lib/session.ts) remplace auth() partout : le rôle et le rattachement adhérent sont relus en base à chaque requête, et users.session_version invalide les jetons émis avant un changement de mot de passe. N'appelez plus auth() directement depuis une page ou une action.
  • Les images ne sont acceptées qu'en data-URI (asImageDataUri) : une URL ferait appeler par le serveur une cible choisie par l'utilisateur (SSRF).
  • La CSP à nonce est posée par src/middleware.ts. Elle impose un rendu dynamique : export const dynamic = "force-dynamic" est dans app/layout.tsx, un HTML pré-généré ne pouvant pas porter de nonce.
  • Les mots de passe temporaires (création d'adhérent, réinitialisation, invitation staff) ne sont jamais stockés : l'action les renvoie et le composant OneTimeCredentials les affiche une seule fois, sans redirection. users.must_change_password seul persiste.
  • Le seed ne crée en production que le référentiel des catégories et l'administrateur initial, avec un mot de passe aléatoire affiché une fois dans les journaux (ou SEED_ADMIN_PASSWORD) et un changement obligatoire à la première connexion. Toutes les données de démonstration (adhérents, promotions, demandes, journal, comptes changeme123) vivent dans src/db/demo-data.ts, exigent SEED_DEMO=true, et npm run db:purge-demo les retire d'une base existante.
  • Sessions JWT limitées à 7 jours (auth.config.ts), HSTS et suppression de X-Powered-By dans next.config.mjs. Le port Postgres de docker-compose n'est publié que sur 127.0.0.1.
  • npm test verrouille ces protections (tests/security.test.ts).

Référencement (SEO)

  • src/lib/seo.ts est pur (constantes, pageMetadata(), générateurs JSON-LD, serializeJsonLd() qui échappe <>&) et verrouillé par tests/seo.test.ts. src/lib/seo-server.ts fournit publicBaseUrl() : réglage site_public_url, puis variables d'environnement, puis en-têtes de la requête.
  • app/layout.tsx pose metadataBase, le gabarit de titre %s · Plein R, Open Graph / Twitter, robots, le manifeste, et les JSON-LD Organization + WebSite. Chaque page publique appelle pageMetadata({ title, description, path }) : le path sert de canonique (l'annuaire ignore ainsi ?q=).
  • Données structurées par page via <JsonLd data={…} /> : BreadcrumbList partout, ItemList sur l'annuaire, LocalBusiness (+ horaires) sur la fiche adhérent, BusinessEvent pour chaque rencontre à venir.
  • app/robots.ts, app/sitemap.ts (pages statiques + adhérents actifs), app/manifest.ts et app/opengraph-image.tsx (vignette 1200×630 générée).
  • /backend, /login, /inscription/* et les fiches non actives sont en NOINDEX ; les pages publiques utilisent <main> et un seul <h1>.

Roles

admin > moderator > editor are staff; member is an adhérent linked to a members row via users.memberId. Capability matrix is in src/lib/rbac.ts.

Docker notes

  • Migrations + optional seed run on container start via docker-entrypoint.sh.
  • npm run build:scripts bundles migrate/seed into dist/*.cjs so the runtime image needs no dev dependencies.
  • The standalone server binds HOSTNAME=0.0.0.0, PORT=3000.

public/assets/logo.svg is a brand-colour recreation; swap in the official asset when available (referenced as /assets/logo.svg).