spb/phi-conseil
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 cookies: z.object({49 title: z.string().min(1),50 description: z.string().min(1),51 acceptAll: z.string().min(1),52 rejectAll: z.string().min(1),53 customize: z.string().min(1),54 save: z.string().min(1),55 back: z.string().min(1),56 manageLabel: z.string().min(1),57 policyLabel: z.string().min(1),58 policyHref: z.string().min(1),59 categories: z60 .array(61 z.object({62 id: z.enum(["necessary", "analytics", "marketing"]),63 label: z.string().min(1),64 description: z.string().min(1),65 }),66 )67 .min(1),68 }),69 seo: z.object({70 siteUrl: z.url(),71 defaultTitle: z.string().min(1),72 titleTemplate: z.string().includes("%s"),73 defaultDescription: z.string().min(1),74 locale: z.string().min(2),75 }),76});7778export const homeSchema = z.object({79 hero: z.object({80 titleLines: z.array(z.string().min(1)).min(1).max(4),81 subtitle: z.string().min(1),82 cta: link,83 image,84 }),85 services: z.object({86 visible: z.boolean(),87 title: z.string().min(1),88 items: z89 .array(90 z.object({91 slug: z.string().regex(/^[a-z0-9-]+$/),92 icon: serviceIconSchema,93 title: z.string().min(1),94 summary: z.string().min(1),95 description: z.string(),96 }),97 )98 .min(1),99 }),100 about: z.object({101 visible: z.boolean(),102 overtitle: z.string().min(1),103 title: z.string().min(1),104 paragraph: z.string().min(1),105 cta: link,106 benefits: z.array(z.string().min(1)).min(1),107 image,108 }),109 testimonials: z.object({110 visible: z.boolean(),111 previewOnly: z.boolean(),112 items: z.array(113 z.object({114 quote: z.string().min(1),115 author: z.string().min(1),116 role: z.string(),117 approved: z.boolean(),118 }),119 ),120 }),121 callToAction: z.object({122 visible: z.boolean(),123 title: z.string().min(1),124 subtitle: z.string().min(1),125 cta: link,126 }),127});128129export const pagesSchema = z.object({130 services: z.object({131 title: z.string().min(1),132 intro: z.string().min(1),133 seoDescription: z.string().min(1),134 }),135 about: z.object({136 title: z.string().min(1),137 intro: z.string().min(1),138 seoDescription: z.string().min(1),139 body: z.array(z.string().min(1)),140 }),141 testimonials: z.object({142 title: z.string().min(1),143 intro: z.string().min(1),144 emptyMessage: z.string().min(1),145 seoDescription: z.string().min(1),146 }),147 contact: z.object({148 title: z.string().min(1),149 intro: z.string().min(1),150 seoDescription: z.string().min(1),151 subjects: z.array(z.string().min(1)).min(1),152 consentLabel: z.string().min(1),153 successTitle: z.string().min(1),154 successMessage: z.string().min(1),155 }),156 privacy: z.object({157 title: z.string().min(1),158 seoDescription: z.string().min(1),159 lastUpdated: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),160 reviewed: z.boolean(),161 }),162});163164export type SiteContent = z.infer<typeof siteSchema>;165export type HomeContent = z.infer<typeof homeSchema>;166export type PagesContent = z.infer<typeof pagesSchema>;167168function parse<T>(schema: z.ZodType<T>, data: unknown, file: string): T {169 const result = schema.safeParse(data);170 if (!result.success) {171 const issues = result.error.issues172 .map((i) => ` - ${i.path.join(".") || "(racine)"}: ${i.message}`)173 .join("\n");174 throw new Error(`Contenu invalide dans content/${file}:\n${issues}`);175 }176 return result.data;177}178179export const site: SiteContent = parse(siteSchema, siteJson, "site.json");180export const home: HomeContent = parse(homeSchema, homeJson, "home.json");181export const pages: PagesContent = parse(pagesSchema, pagesJson, "pages.json");182183/** Témoignages publiables : section visible ET avis approuvés uniquement. */184export function publishedTestimonials(): HomeContent["testimonials"]["items"] {185 if (!home.testimonials.visible) return [];186 return home.testimonials.items.filter((t) => t.approved);187}188189/**190 * Témoignages à afficher : en production, uniquement les avis approuvés ;191 * en prévisualisation (NEXT_PUBLIC_CONTENT_PREVIEW=1), les exemples non192 * vérifiés sont montrés avec un bandeau explicite.193 */194export function displayableTestimonials(preview: boolean) {195 const approved = publishedTestimonials();196 if (approved.length > 0) return { items: approved, isPreview: false };197 if (preview && home.testimonials.previewOnly) {198 return { items: home.testimonials.items, isPreview: true };199 }200 return { items: [], isPreview: false };201}202203export function serviceBySlug(slug: string) {204 return home.services.items.find((s) => s.slug === slug);205}206