Repository Public
1import { z } from "zod";2import siteJson from "../../content/site.json";3import homeJson from "../../content/home.json";4import pagesJson from "../../content/pages.json";56/**7 * Unique point d'accès au contenu administrable.8 * Les fichiers JSON de `content/` sont validés au chargement : une erreur de9 * saisie (champ manquant, type incorrect) fait échouer le build plutôt que de10 * publier une page cassée.11 *12 * Pour brancher un CMS (Payload, Sanity…), remplacer les imports JSON par des13 * appels à l'API du CMS en conservant les schémas ci-dessous.14 */1516const link = z.object({17 label: z.string().min(1),18 href: z.string().min(1),19});2021const image = z.object({22 src: z.string(), // vide = illustration vectorielle de remplacement23 alt: z.string().min(1),24});2526export const serviceIconSchema = z.enum(["search", "document-edit", "chart", "check-circle"]);27export type ServiceIcon = z.infer<typeof serviceIconSchema>;2829export const siteSchema = z.object({30 brand: z.object({31 name: z.string().min(1),32 tagline: z.string().min(1),33 footerSignature: z.string().min(1),34 legalName: z.string().min(1),35 }),36 navigation: z.array(link).min(1),37 headerCta: link,38 contact: z.object({39 email: z.union([z.literal(""), z.email()]),40 phone: z.string(),41 address: z.string(),42 region: z.string(),43 }),44 social: z.object({45 linkedin: z.union([z.literal(""), z.url()]),46 }),47 legalLinks: z.array(link),48 seo: z.object({49 siteUrl: z.url(),50 defaultTitle: z.string().min(1),51 titleTemplate: z.string().includes("%s"),52 defaultDescription: z.string().min(1),53 locale: z.string().min(2),54 }),55});5657export const homeSchema = z.object({58 hero: z.object({59 titleLines: z.array(z.string().min(1)).min(1).max(4),60 subtitle: z.string().min(1),61 cta: link,62 image,63 }),64 services: z.object({65 visible: z.boolean(),66 title: z.string().min(1),67 items: z68 .array(69 z.object({70 slug: z.string().regex(/^[a-z0-9-]+$/),71 icon: serviceIconSchema,72 title: z.string().min(1),73 summary: z.string().min(1),74 description: z.string(),75 }),76 )77 .min(1),78 }),79 about: z.object({80 visible: z.boolean(),81 overtitle: z.string().min(1),82 title: z.string().min(1),83 paragraph: z.string().min(1),84 cta: link,85 benefits: z.array(z.string().min(1)).min(1),86 image,87 }),88 testimonials: z.object({89 visible: z.boolean(),90 previewOnly: z.boolean(),91 items: z.array(92 z.object({93 quote: z.string().min(1),94 author: z.string().min(1),95 role: z.string(),96 approved: z.boolean(),97 }),98 ),99 }),100 callToAction: z.object({101 visible: z.boolean(),102 title: z.string().min(1),103 subtitle: z.string().min(1),104 cta: link,105 }),106});107108export const pagesSchema = z.object({109 services: z.object({110 title: z.string().min(1),111 intro: z.string().min(1),112 seoDescription: z.string().min(1),113 }),114 about: z.object({115 title: z.string().min(1),116 intro: z.string().min(1),117 seoDescription: z.string().min(1),118 body: z.array(z.string().min(1)),119 }),120 testimonials: z.object({121 title: z.string().min(1),122 intro: z.string().min(1),123 emptyMessage: z.string().min(1),124 seoDescription: z.string().min(1),125 }),126 contact: z.object({127 title: z.string().min(1),128 intro: z.string().min(1),129 seoDescription: z.string().min(1),130 subjects: z.array(z.string().min(1)).min(1),131 consentLabel: z.string().min(1),132 successTitle: z.string().min(1),133 successMessage: z.string().min(1),134 }),135 privacy: z.object({136 title: z.string().min(1),137 seoDescription: z.string().min(1),138 lastUpdated: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),139 reviewed: z.boolean(),140 }),141});142143export type SiteContent = z.infer<typeof siteSchema>;144export type HomeContent = z.infer<typeof homeSchema>;145export type PagesContent = z.infer<typeof pagesSchema>;146147function parse<T>(schema: z.ZodType<T>, data: unknown, file: string): T {148 const result = schema.safeParse(data);149 if (!result.success) {150 const issues = result.error.issues151 .map((i) => ` - ${i.path.join(".") || "(racine)"}: ${i.message}`)152 .join("\n");153 throw new Error(`Contenu invalide dans content/${file}:\n${issues}`);154 }155 return result.data;156}157158export const site: SiteContent = parse(siteSchema, siteJson, "site.json");159export const home: HomeContent = parse(homeSchema, homeJson, "home.json");160export const pages: PagesContent = parse(pagesSchema, pagesJson, "pages.json");161162/** Témoignages publiables : section visible ET avis approuvés uniquement. */163export function publishedTestimonials(): HomeContent["testimonials"]["items"] {164 if (!home.testimonials.visible) return [];165 return home.testimonials.items.filter((t) => t.approved);166}167168/**169 * Témoignages à afficher : en production, uniquement les avis approuvés ;170 * en prévisualisation (NEXT_PUBLIC_CONTENT_PREVIEW=1), les exemples non171 * vérifiés sont montrés avec un bandeau explicite.172 */173export function displayableTestimonials(preview: boolean) {174 const approved = publishedTestimonials();175 if (approved.length > 0) return { items: approved, isPreview: false };176 if (preview && home.testimonials.previewOnly) {177 return { items: home.testimonials.items, isPreview: true };178 }179 return { items: [], isPreview: false };180}181182export function serviceBySlug(slug: string) {183 return home.services.items.find((s) => s.slug === slug);184}185