rubis/apps/landing/src/layouts/Layout.astro
ordinarthur c590b489ef
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 58s
change logo
2026-05-11 23:29:09 +02:00

160 lines
6.7 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
/**
* 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
* <slot /> 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]) : [];
---
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{fullTitle}</title>
<meta name="description" content={description} />
<meta name="robots" content={robots} />
<meta name="theme-color" content="#9F1239" />
<meta name="author" content="Rubis sur l'ongle" />
<link rel="canonical" href={url} />
<link rel="alternate" hreflang="fr-FR" href={url} />
<link rel="alternate" hreflang="x-default" href={url} />
{/* Open Graph */}
<meta property="og:site_name" content="Rubis sur l'ongle" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:type" content={ogType} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:image" content={resolvedOgImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={title} />
{/* 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. */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={resolvedOgImage} />
{/* 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é. */}
<link rel="preload" as="font" type="font/woff2" crossorigin="anonymous" href={interLatinWoff2} />
<link rel="preload" as="font" type="font/woff2" crossorigin="anonymous" href={bricolageLatinWoff2} />
{/* 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. */}
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="apple-mobile-web-app-title" content="Rubis" />
{/* 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. */}
<link rel="alternate" type="application/rss+xml" title="Blog Rubis" href={`${SITE_URL}/blog/rss.xml`} />
<link rel="alternate" type="application/rss+xml" title="Changelog Rubis" href={`${SITE_URL}/changelog/rss.xml`} />
{/* JSON-LD structured data */}
{
jsonLdArray.map((data) => (
<script is:inline type="application/ld+json" set:html={JSON.stringify(data)} />
))
}
</head>
<body>
<SiteHeader solid={solidHeader} />
<main>
<slot />
</main>
<SiteFooter />
</body>
</html>