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>
426 lines
13 KiB
HTML
426 lines
13 KiB
HTML
<!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>
|