fix(seo): smart title suffix + OG image par défaut 1200×630
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m1s
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m1s
Audit SEO révélait deux pertes de valeur :
1. **Titres trop longs** (86 chars sur la home, 71 chars sur les articles).
Google tronque à ~60 chars dans le SERP. Le suffixe automatique
`— Rubis sur l'ongle` (20 chars) écrasait le message-clé.
Layout.astro fait maintenant un suffix smart :
* Si title <45 chars ET ne contient pas "Rubis" → suffix `— Rubis` (8 chars)
* Sinon → titre tel quel (la brand est déjà couverte par og:site_name +
JSON-LD publisher + le hostname rubis.pro visible dans la SERP).
Résultat : home 64 chars, article 51 chars, "Mentions légales" → 24 chars
avec suffix.
2. **Pas d'`og:image` sur les pages sans hero** (home, légal). Sur
LinkedIn/X/Slack, aucune preview image — perte d'engagement énorme.
Ajout d'un og-default.png 1200×630 (165 KB optimisé) dans
apps/landing/public/, monté par fallback par Layout.astro quand la
page ne fournit pas d'`ogImage` explicite. Twitter:card devient
toujours `summary_large_image`.
L'image a été générée via le nouvel outil HTML
docs/marketing/assets/og-default.html (clone de la mécanique du
linkedin-banner.html — clic sur "Télécharger PNG" et go).
Compose : brand + tagline + mock card 124 rubis + CTA rubis.pro.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
b2dd991c58
commit
eda5436d12
BIN
apps/landing/public/og-default.png
Normal file
BIN
apps/landing/public/og-default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
@ -16,12 +16,20 @@ import { SiteFooter } from "../components/SiteFooter";
|
||||
|
||||
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). */
|
||||
/** 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";
|
||||
@ -44,7 +52,20 @@ const {
|
||||
jsonLd,
|
||||
} = Astro.props;
|
||||
|
||||
const fullTitle = title.includes("Rubis") ? title : `${title} — Rubis sur l'ongle`;
|
||||
/**
|
||||
* 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]) : [];
|
||||
@ -72,22 +93,17 @@ const jsonLdArray = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
|
||||
<meta property="og:title" content={fullTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={url} />
|
||||
{
|
||||
ogImage && (
|
||||
<>
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content={title} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<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 */}
|
||||
<meta name="twitter:card" content={ogImage ? "summary_large_image" : "summary"} />
|
||||
{/* 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} />
|
||||
{ogImage && <meta name="twitter:image" content={ogImage} />}
|
||||
<meta name="twitter:image" content={resolvedOgImage} />
|
||||
|
||||
{/* Favicons */}
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
|
||||
425
docs/marketing/assets/og-default.html
Normal file
425
docs/marketing/assets/og-default.html
Normal file
@ -0,0 +1,425 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
OG image par défaut — 1200 × 630 px (ratio 1.91:1, standard Open Graph).
|
||||
Servie sur rubis.pro/og-default.png par Astro static (apps/landing/public/).
|
||||
|
||||
Pourquoi ce ratio :
|
||||
- Facebook / LinkedIn / Twitter / Slack utilisent tous le carrousel
|
||||
1200 × 630 (~1.91:1) pour les preview cards.
|
||||
- Les images plus larges (LinkedIn banner 1584 × 280) sont rognées en
|
||||
preview car le viewport square les recoupe.
|
||||
|
||||
Ouvre ce fichier dans Chrome et clique :
|
||||
- "Télécharger PNG" → og-default.png, prêt à poser dans
|
||||
apps/landing/public/og-default.png
|
||||
|
||||
La page est volontairement BUSY (mock card visible, tagline complète) —
|
||||
l'objectif est de transmettre le produit en un coup d'œil quand quelqu'un
|
||||
partage rubis.pro sur LinkedIn / X / Slack / iMessage.
|
||||
-->
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OG image par défaut — Rubis sur l'ongle</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--rubis: #9f1239;
|
||||
--rubis-deep: #771328;
|
||||
--rubis-light: #c9415c;
|
||||
--rubis-glow: #fbe4ea;
|
||||
--ink: #1a1410;
|
||||
--ink-2: #4f4640;
|
||||
--ink-3: #8a7f76;
|
||||
--line: #e8e0d6;
|
||||
--cream: #faf7f2;
|
||||
--cream-2: #f5efe7;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: #d9d4cb;
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
.controls h2 {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: #1a1410;
|
||||
margin: 0 16px 0 0;
|
||||
letter-spacing: -0.012em;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #9f1239;
|
||||
color: white;
|
||||
box-shadow: 0 2px 6px rgba(159, 18, 57, 0.25);
|
||||
}
|
||||
.btn-primary:hover { background: #771328; transform: translateY(-1px); }
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: #1a1410;
|
||||
border: 1px solid #e8e0d6;
|
||||
}
|
||||
.btn-secondary:hover { background: #faf7f2; }
|
||||
.btn:disabled { opacity: 0.6; cursor: wait; }
|
||||
.hint { font-size: 12.5px; color: #4f4640; margin-left: 8px; }
|
||||
|
||||
/* ===== OG image — 1200 × 630 ===== */
|
||||
.og {
|
||||
width: 1200px;
|
||||
height: 630px;
|
||||
background: var(--cream);
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 0.85fr;
|
||||
gap: 56px;
|
||||
padding: 64px 72px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Halo rubis discret en haut-droite */
|
||||
.og::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -240px;
|
||||
right: -180px;
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
background: radial-gradient(circle, var(--rubis-glow) 0%, transparent 65%);
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Gem watermark très transparent en pied */
|
||||
.og::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -200px;
|
||||
left: 25%;
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
transform: rotate(45deg);
|
||||
background: var(--rubis);
|
||||
opacity: 0.04;
|
||||
border-radius: 28px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============== LEFT — brand + tagline ============== */
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
max-width: 580px;
|
||||
}
|
||||
.brand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.gem-svg {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
filter: drop-shadow(0 2px 4px rgba(159, 18, 57, 0.18));
|
||||
}
|
||||
.brand-name {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 28px;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
.brand-name .suffix {
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
color: var(--ink-3);
|
||||
font-size: 18px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 56px;
|
||||
color: var(--ink);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.028em;
|
||||
}
|
||||
.tagline em {
|
||||
font-style: italic;
|
||||
color: var(--rubis);
|
||||
}
|
||||
|
||||
.pitch {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
color: var(--ink-2);
|
||||
line-height: 1.5;
|
||||
max-width: 480px;
|
||||
}
|
||||
.pitch b {
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.url {
|
||||
margin-top: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: var(--ink-2);
|
||||
background: white;
|
||||
padding: 10px 18px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
align-self: flex-start;
|
||||
box-shadow: 0 2px 8px rgba(26, 20, 16, 0.04);
|
||||
}
|
||||
.url::before {
|
||||
content: "";
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background: var(--rubis);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* ============== RIGHT — mock card ============== */
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
background: white;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
padding: 28px 30px;
|
||||
box-shadow:
|
||||
0 24px 48px -16px rgba(26, 20, 16, 0.18),
|
||||
0 6px 12px -4px rgba(26, 20, 16, 0.08);
|
||||
}
|
||||
|
||||
.rubis-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.rubis-hero .gem-big {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
filter: drop-shadow(0 4px 10px rgba(159, 18, 57, 0.32));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rubis-count {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.022em;
|
||||
line-height: 1;
|
||||
}
|
||||
.rubis-sub {
|
||||
font-size: 14px;
|
||||
color: var(--ink-2);
|
||||
margin-top: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.rubis-sub b { font-weight: 600; color: var(--ink); }
|
||||
|
||||
.kpis {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 22px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.kpi-label {
|
||||
font-size: 11px;
|
||||
color: var(--ink-3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.kpi-value {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.015em;
|
||||
margin-top: 5px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.kpi-delta {
|
||||
font-size: 12px;
|
||||
color: var(--rubis);
|
||||
font-weight: 500;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.activity {
|
||||
margin-top: 18px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed var(--line);
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
.activity b { color: var(--ink); font-weight: 600; }
|
||||
.activity time { color: var(--ink-3); font-size: 11.5px; font-variant-numeric: tabular-nums; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="controls">
|
||||
<h2>OG image — 1200 × 630</h2>
|
||||
<button id="exportPng" class="btn btn-primary">🖼️ Télécharger og-default.png</button>
|
||||
<span class="hint">Pose ensuite dans <code>apps/landing/public/og-default.png</code></span>
|
||||
</div>
|
||||
|
||||
<div class="og">
|
||||
<!-- ============ LEFT ============ -->
|
||||
<div class="left">
|
||||
<div class="brand-row">
|
||||
<svg class="gem-svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1" />
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8" />
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65" />
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48" />
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<span class="brand-name">Rubis<span class="suffix">sur l'ongle</span></span>
|
||||
</div>
|
||||
|
||||
<h1 class="tagline">
|
||||
Vos factures relancées <em>toutes seules</em>.
|
||||
</h1>
|
||||
|
||||
<p class="pitch">
|
||||
Le SaaS de relance pour TPE-PME françaises. <b>5 heures par semaine</b>
|
||||
récupérées, automatiquement.
|
||||
</p>
|
||||
|
||||
<div class="url">rubis.pro</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ RIGHT — mock card ============ -->
|
||||
<div class="right">
|
||||
<div class="card">
|
||||
<div class="rubis-hero">
|
||||
<svg class="gem-big" viewBox="0 0 100 100">
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1" />
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8" />
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65" />
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48" />
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div>
|
||||
<div class="rubis-count">124 rubis</div>
|
||||
<div class="rubis-sub">≈ <b>24 h 48</b> libérées ce mois</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpis">
|
||||
<div>
|
||||
<div class="kpi-label">Encaissé</div>
|
||||
<div class="kpi-value">14 320 €</div>
|
||||
<div class="kpi-delta">+ 2 800 € vs avril</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="kpi-label">DSO</div>
|
||||
<div class="kpi-value">38 j</div>
|
||||
<div class="kpi-delta">↘ −6 j depuis Rubis</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity">
|
||||
<span>✓ Facture <b>F-2024-035</b> encaissée</span>
|
||||
<time>10:02</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lib CDN pour export PNG -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js"></script>
|
||||
<script>
|
||||
const og = document.querySelector(".og");
|
||||
const btnPng = document.getElementById("exportPng");
|
||||
|
||||
// Capture en 2× pour rendu net, puis le navigateur télécharge en 2400 × 1260
|
||||
// (LinkedIn / Twitter / Facebook downsamplent au format final 1200 × 630).
|
||||
const captureOptions = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
canvasWidth: 1200 * 2,
|
||||
canvasHeight: 630 * 2,
|
||||
pixelRatio: 2,
|
||||
backgroundColor: "#FAF7F2",
|
||||
cacheBust: true,
|
||||
};
|
||||
|
||||
btnPng.addEventListener("click", async () => {
|
||||
btnPng.disabled = true;
|
||||
btnPng.textContent = "⏳ Génération…";
|
||||
try {
|
||||
await document.fonts.ready;
|
||||
const dataUrl = await htmlToImage.toPng(og, captureOptions);
|
||||
const link = document.createElement("a");
|
||||
link.download = "og-default.png";
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
btnPng.textContent = "✓ PNG téléchargé";
|
||||
setTimeout(() => {
|
||||
btnPng.textContent = "🖼️ Télécharger og-default.png";
|
||||
btnPng.disabled = false;
|
||||
}, 1800);
|
||||
} catch (err) {
|
||||
console.error("Export PNG :", err);
|
||||
alert("Erreur export PNG : " + err.message);
|
||||
btnPng.disabled = false;
|
||||
btnPng.textContent = "🖼️ Télécharger og-default.png";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user