Files
PleinR/src/app/backend/parametres/page.tsx
T
Claude b860eeaf29 Référencement : Google Analytics et Search Console réglables en backoffice
Deux réglages dans Backend › Paramètres :

- `google_analytics_id` pose les balises gtag.js. Vide = rien n'est chargé.
  `src/lib/analytics.ts` (pur) n'accepte qu'un identifiant GA4 « G-… » : la
  saisie ne peut donc pas s'échapper du script en ligne. Les deux `<script>`
  portent le nonce de la CSP, et seules les pages publiques sont mesurées
  (`isTrackedPath` écarte `PRIVATE_PATHS`, le chemin venant du nouvel en-tête
  `x-pathname` du middleware — un layout ne connaît pas l'URL demandée).
- `google_site_verification` rend la balise de validation Search Console via
  `verification.google`, après contrôle du jeton.

La CSP gagne les hôtes Google Analytics en `connect-src` : elle est posée sur
le runtime Edge, qui ne peut pas lire la base pour savoir si la mesure est
active. `script-src` est inchangé, `'strict-dynamic'` couvrant les scripts que
gtag.js charge ensuite.

Le plan du site n'a besoin de rien : `/sitemap.xml` est déjà servi et annoncé
dans `/robots.txt`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7v5KcYUo2taDVDgehs7ZH
2026-09-15 05:12:21 +00:00

184 lines
8.1 KiB
TypeScript

import { redirect } from "next/navigation";
import type { CSSProperties } from "react";
import { getSession } from "@/lib/session";
import { ImageField } from "@/components/ImageField";
import { can } from "@/lib/rbac";
import { getSiteSettings } from "@/lib/site-settings";
import { saveSiteSettings } from "../actions";
export const dynamic = "force-dynamic";
export default async function ParametresPage() {
const session = await getSession();
if (!can(session?.user.role, "manageSettings")) redirect("/backend");
const settings = await getSiteSettings();
return (
<form action={saveSiteSettings} style={{ display: "grid", gap: 22, maxWidth: 1080 }}>
<section style={panel}>
<h2 className="font-display" style={title}>Identité du site</h2>
<div className="grid grid-2" style={{ gap: 16 }}>
<Field label="Nom association" name="association_name" value={settings.association_name} />
<Field label="Slogan" name="association_tagline" value={settings.association_tagline} />
<Field label="E-mail" name="association_email" value={settings.association_email} />
<Field label="Téléphone" name="association_phone" value={settings.association_phone} />
<Field label="Site web" name="association_website" value={settings.association_website} />
<Field label="Contact principal" name="association_contact" value={settings.association_contact} />
<Field
label="Page Facebook"
name="association_facebook"
value={settings.association_facebook}
placeholder="https://www.facebook.com/..."
/>
<Field
label="Page LinkedIn"
name="association_linkedin"
value={settings.association_linkedin}
placeholder="https://www.linkedin.com/company/..."
/>
<div style={{ gridColumn: "1 / -1" }}>
<Field label="Adresse" name="association_address" value={settings.association_address} />
</div>
<Area label="Texte d'introduction Association" name="association_intro" value={settings.association_intro} />
<Area
label="Mission (une ligne vide sépare les paragraphes)"
name="association_mission"
value={settings.association_mission}
/>
<Area
label="Ce que l'association apporte (Titre|Description, une ligne par point)"
name="association_pillars"
value={settings.association_pillars}
/>
</div>
<div style={{ marginTop: 14, color: "#9a6638", fontSize: 13, lineHeight: 1.6 }}>
Les pages Facebook et LinkedIn s&apos;affichent sur la page d&apos;accueil et dans le pied
de page. Laissez un champ vide pour masquer le réseau correspondant. La publication
automatique des promotions, elle, se configure côté serveur (jetons d&apos;accès dans les
variables d&apos;environnement).
</div>
</section>
<section style={panel}>
<h2 className="font-display" style={title}>Directoire</h2>
<div className="grid grid-2" style={{ gap: 16 }}>
<Field label="Président(e)" name="association_president" value={settings.association_president} />
<Field label="Intitulé du rôle" name="association_president_role" value={settings.association_president_role} />
<Field label="Vice-président(e)" name="association_vice_president" value={settings.association_vice_president} />
<Field label="Trésorier(ère)" name="association_treasurer" value={settings.association_treasurer} />
<Field label="Secrétaire" name="association_secretary" value={settings.association_secretary} />
<ImageField name="association_president_photo" label="Photo président(e)" defaultValue={settings.association_president_photo} height={130} />
<Area label="Message président(e)" name="association_president_message" value={settings.association_president_message} />
<Area
label="Autres membres du directoire (Nom|Fonction, une ligne par personne)"
name="association_board_members"
value={settings.association_board_members}
/>
</div>
</section>
<section style={panel}>
<h2 className="font-display" style={title}>Mentions légales & confidentialité</h2>
<div className="grid grid-2" style={{ gap: 16 }}>
<Field label="SIRET" name="association_siret" value={settings.association_siret} />
<Field label="RNA" name="association_rna" value={settings.association_rna} />
<Field label="Directeur de publication" name="association_publication_director" value={settings.association_publication_director} />
<Field label="Contact confidentialité/RGPD" name="association_privacy_contact" value={settings.association_privacy_contact} />
<Field label="Hébergeur" name="association_host_name" value={settings.association_host_name} />
<Field label="Téléphone hébergeur" name="association_host_phone" value={settings.association_host_phone} />
<div style={{ gridColumn: "1 / -1" }}>
<Field label="Adresse hébergeur" name="association_host_address" value={settings.association_host_address} />
</div>
<Field label="Dernière mise à jour légale" name="legal_updated" value={settings.legal_updated} placeholder="17 juillet 2026" />
</div>
<div style={{ marginTop: 14, color: "#9a6638", fontSize: 13, lineHeight: 1.6 }}>
Remplissez ces champs avant mise en production: ils alimentent les pages publiques
Mentions légales et Confidentialité.
</div>
</section>
<section style={panel}>
<h2 className="font-display" style={title}>Référencement & mesure d&apos;audience</h2>
<div className="grid grid-2" style={{ gap: 16 }}>
<Field
label="Identifiant Google Analytics (GA4)"
name="google_analytics_id"
value={settings.google_analytics_id}
placeholder="G-XXXXXXXXXX"
/>
<Field
label="Jeton Search Console (balise HTML)"
name="google_site_verification"
value={settings.google_site_verification}
placeholder="Valeur de l'attribut content"
/>
</div>
<div style={{ marginTop: 14, color: "#9a6638", fontSize: 13, lineHeight: 1.6 }}>
Laissez vide pour ne rien charger : sans identifiant, aucune balise Google n&apos;est
posée sur le site. La mesure d&apos;audience ne couvre que les pages publiques — ni le
backoffice, ni l&apos;espace adhérent. Le plan du site est déjà publié sur{" "}
<code>/sitemap.xml</code> et annoncé dans <code>/robots.txt</code> : il suffit de le
déclarer dans Search Console. Pensez à mentionner Google Analytics dans la page
Confidentialité avant de l&apos;activer.
</div>
</section>
<button type="submit" style={submitButton}>Enregistrer les paramètres</button>
</form>
);
}
function Field({
label,
name,
value,
placeholder,
}: {
label: string;
name: string;
value: string;
placeholder?: string;
}) {
return (
<div>
<label className="field-label">{label}</label>
<input name={name} defaultValue={value} placeholder={placeholder} className="field" />
</div>
);
}
function Area({ label, name, value }: { label: string; name: string; value: string }) {
return (
<div style={{ gridColumn: "1 / -1" }}>
<label className="field-label">{label}</label>
<textarea name={name} rows={4} defaultValue={value} className="field" />
</div>
);
}
const panel: CSSProperties = {
background: "#fff",
border: "1px solid #e6dcc6",
borderRadius: 16,
padding: 22,
};
const title: CSSProperties = {
margin: "0 0 16px",
fontSize: 20,
color: "#26201a",
};
const submitButton: CSSProperties = {
justifySelf: "start",
border: "none",
background: "#13324F",
color: "#fff",
fontWeight: 800,
fontSize: 14.5,
padding: "13px 22px",
borderRadius: 10,
cursor: "pointer",
};