rubis/docs/marketing/assets/og-default.html
ordinarthur eda5436d12
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m1s
fix(seo): smart title suffix + OG image par défaut 1200×630
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>
2026-05-09 18:14:28 +02:00

426 lines
13 KiB
HTML
Raw Permalink 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.

<!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>