--- /** * Layout commun à toutes les pages rubis.pro/*. * * Reçoit en props les meta SEO essentiels. Importe le CSS racine (qui inline * les tokens + base @rubis/ui), monte SiteHeader + SiteFooter, et expose un * pour le contenu de la page. * * Convention : zéro JS hydraté côté public sauf nécessité explicite — le * SiteHeader + SiteFooter sont rendus côté serveur uniquement (pas de * client:load) pour rester sur le bundle minimal. */ import "../styles/app.css"; import { SiteHeader } from "../components/SiteHeader"; import { SiteFooter } from "../components/SiteFooter"; /** * URLs hashées (au build) des deux woff2 latin que la quasi-totalité du * contenu utilise — Inter (body) + Bricolage Grotesque (display). Le * suffix `?url` Vite retourne le path final après hashing, donc le * preload reste valide même après un rebuild qui change le hash. * * Préchargées en HEAD pour casser la chaîne `HTML → CSS → fonts` du * critical path (cf. audit Lighthouse network-dependency-tree, ~50 ms * gagnés sur le LCP). On NE preload PAS les latin-ext / vietnamese / * cyrillic / greek : poids inutile pour 99% du trafic FR/latin. */ import interLatinWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url"; import bricolageLatinWoff2 from "@fontsource-variable/bricolage-grotesque/files/bricolage-grotesque-latin-wght-normal.woff2?url"; const SITE_URL = "https://rubis.pro"; /** * OG image par défaut (1200×630) servie depuis apps/landing/public/. * Utilisée pour les partages sociaux quand aucune image n'est fournie * par la page (cas de la home + pages légales). Les articles de blog * ont leur propre hero qui prend précédence. */ const DEFAULT_OG_IMAGE = `${SITE_URL}/og-default.png`; interface Props { title: string; description: string; /** Path absolu de la page courante (ex. "/blog/foo"). Default = location actuelle. */ pathname?: string; /** OG image absolue (1200×630 idéal). Fallback sur og-default.png si non fournie. */ ogImage?: string; /** OG type. Default "website". Mettre "article" sur les pages de blog. */ ogType?: "website" | "article"; /** Si true → noindex (legal/test). */ noindex?: boolean; /** Header solide (bordure + fond) plutôt que transparent (par défaut). */ solidHeader?: boolean; /** JSON-LD structured data — passé en string déjà sérialisé OU en object. */ jsonLd?: object | object[]; } const { title, description, pathname, ogImage, ogType = "website", noindex = false, solidHeader = false, jsonLd, } = Astro.props; /** * Suffixe brand intelligent : * - Si le titre est court (<45 chars) ET ne contient pas "Rubis", on ajoute * " — Rubis" pour la cohérence brand dans les SERP. * - Sinon (titre long ou déjà brandé), on laisse tel quel : Google tronque * à ~60 chars dans le snippet, autant garder le message-clé en entier. * * Le branding reste assuré par og:site_name + JSON-LD publisher + le hostname * rubis.pro visible dans la SERP — pas besoin de le marteler dans le title. */ const fullTitle = title.length < 45 && !title.includes("Rubis") ? `${title} — Rubis` : title; const resolvedOgImage = ogImage ?? DEFAULT_OG_IMAGE; const url = `${SITE_URL}${pathname ?? Astro.url.pathname}`; const robots = noindex ? "noindex,nofollow" : "index,follow,max-image-preview:large"; const jsonLdArray = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : []; --- {fullTitle} {/* Open Graph */} {/* Twitter Card — toujours summary_large_image puisqu'on a maintenant un og-default.png 1200×630 servi par défaut sur les pages sans hero. */} {/* Preload des fonts critiques (latin uniquement) — coupe la chaîne HTML→CSS→woff2 en faisant démarrer le téléchargement au plus tôt. crossorigin="anonymous" est obligatoire pour matcher la requête woff2 que Vite émet derrière (sans cred), sinon le browser refait un round-trip et le preload est ignoré. */} {/* Favicons — set aligné sur apps/web (mêmes assets sur rubis.pro et app.rubis.pro, cohérence visuelle quand l'user passe de la landing à l'app). SVG hand-coded en priorité (1 KB), PNG en fallback iOS. `favicon.ico` (legacy IE/Edge bookmark) gardé pour les bookmark bars. */} {/* RSS auto-discovery — exposés sur toutes les pages pour que les lecteurs RSS détectent les deux flux quel que soit le point d'entrée. */} {/* JSON-LD structured data */} { jsonLdArray.map((data) => (