SPB Git

Repository Public

content.ts 5.4 KB 185 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  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
Files 18a9e1379a7e4662b184e2c070c92578c5de3ea8
Menu

Theme