import { z } from "zod"; import siteJson from "../../content/site.json"; import homeJson from "../../content/home.json"; import pagesJson from "../../content/pages.json"; /** * Unique point d'accès au contenu administrable. * Les fichiers JSON de `content/` sont validés au chargement : une erreur de * saisie (champ manquant, type incorrect) fait échouer le build plutôt que de * publier une page cassée. * * Pour brancher un CMS (Payload, Sanity…), remplacer les imports JSON par des * appels à l'API du CMS en conservant les schémas ci-dessous. */ const link = z.object({ label: z.string().min(1), href: z.string().min(1), }); const image = z.object({ src: z.string(), // vide = illustration vectorielle de remplacement alt: z.string().min(1), }); export const serviceIconSchema = z.enum(["search", "document-edit", "chart", "check-circle"]); export type ServiceIcon = z.infer; export const siteSchema = z.object({ brand: z.object({ name: z.string().min(1), tagline: z.string().min(1), footerSignature: z.string().min(1), legalName: z.string().min(1), }), navigation: z.array(link).min(1), headerCta: link, contact: z.object({ email: z.union([z.literal(""), z.email()]), phone: z.string(), address: z.string(), region: z.string(), }), social: z.object({ linkedin: z.union([z.literal(""), z.url()]), }), legalLinks: z.array(link), cookies: z.object({ title: z.string().min(1), description: z.string().min(1), acceptAll: z.string().min(1), rejectAll: z.string().min(1), customize: z.string().min(1), save: z.string().min(1), back: z.string().min(1), manageLabel: z.string().min(1), policyLabel: z.string().min(1), policyHref: z.string().min(1), categories: z .array( z.object({ id: z.enum(["necessary", "analytics", "marketing"]), label: z.string().min(1), description: z.string().min(1), }), ) .min(1), }), seo: z.object({ siteUrl: z.url(), defaultTitle: z.string().min(1), titleTemplate: z.string().includes("%s"), defaultDescription: z.string().min(1), locale: z.string().min(2), }), }); export const homeSchema = z.object({ hero: z.object({ titleLines: z.array(z.string().min(1)).min(1).max(4), subtitle: z.string().min(1), cta: link, image, }), services: z.object({ visible: z.boolean(), title: z.string().min(1), items: z .array( z.object({ slug: z.string().regex(/^[a-z0-9-]+$/), icon: serviceIconSchema, title: z.string().min(1), summary: z.string().min(1), description: z.string(), }), ) .min(1), }), about: z.object({ visible: z.boolean(), overtitle: z.string().min(1), title: z.string().min(1), paragraph: z.string().min(1), cta: link, benefits: z.array(z.string().min(1)).min(1), image, }), testimonials: z.object({ visible: z.boolean(), previewOnly: z.boolean(), items: z.array( z.object({ quote: z.string().min(1), author: z.string().min(1), role: z.string(), approved: z.boolean(), }), ), }), callToAction: z.object({ visible: z.boolean(), title: z.string().min(1), subtitle: z.string().min(1), cta: link, }), }); export const pagesSchema = z.object({ services: z.object({ title: z.string().min(1), intro: z.string().min(1), seoDescription: z.string().min(1), }), about: z.object({ title: z.string().min(1), intro: z.string().min(1), seoDescription: z.string().min(1), body: z.array(z.string().min(1)), }), testimonials: z.object({ title: z.string().min(1), intro: z.string().min(1), emptyMessage: z.string().min(1), seoDescription: z.string().min(1), }), contact: z.object({ title: z.string().min(1), intro: z.string().min(1), seoDescription: z.string().min(1), subjects: z.array(z.string().min(1)).min(1), consentLabel: z.string().min(1), successTitle: z.string().min(1), successMessage: z.string().min(1), }), privacy: z.object({ title: z.string().min(1), seoDescription: z.string().min(1), lastUpdated: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), reviewed: z.boolean(), }), }); export type SiteContent = z.infer; export type HomeContent = z.infer; export type PagesContent = z.infer; function parse(schema: z.ZodType, data: unknown, file: string): T { const result = schema.safeParse(data); if (!result.success) { const issues = result.error.issues .map((i) => ` - ${i.path.join(".") || "(racine)"}: ${i.message}`) .join("\n"); throw new Error(`Contenu invalide dans content/${file}:\n${issues}`); } return result.data; } export const site: SiteContent = parse(siteSchema, siteJson, "site.json"); export const home: HomeContent = parse(homeSchema, homeJson, "home.json"); export const pages: PagesContent = parse(pagesSchema, pagesJson, "pages.json"); /** Témoignages publiables : section visible ET avis approuvés uniquement. */ export function publishedTestimonials(): HomeContent["testimonials"]["items"] { if (!home.testimonials.visible) return []; return home.testimonials.items.filter((t) => t.approved); } /** * Témoignages à afficher : en production, uniquement les avis approuvés ; * en prévisualisation (NEXT_PUBLIC_CONTENT_PREVIEW=1), les exemples non * vérifiés sont montrés avec un bandeau explicite. */ export function displayableTestimonials(preview: boolean) { const approved = publishedTestimonials(); if (approved.length > 0) return { items: approved, isPreview: false }; if (preview && home.testimonials.previewOnly) { return { items: home.testimonials.items, isPreview: true }; } return { items: [], isPreview: false }; } export function serviceBySlug(slug: string) { return home.services.items.find((s) => s.slug === slug); }