SPB Git

spb/phi-conseil

Public
5.9 KB · 206 lines typescript
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
Files · main
Menu

Theme