All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 58s
160 lines
6.7 KiB
Plaintext
160 lines
6.7 KiB
Plaintext
---
|
||
/**
|
||
* 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>
|