rubis/docs/marketing/assets/linkedin-banner.html
ordinarthur c3c9dbb408
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 27s
add des betises
2026-05-09 20:11:33 +02:00

518 lines
16 KiB
HTML
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.

<!doctype html>
<!--
Bannière LinkedIn personnelle — 1584 × 396 px (ratio 4:1).
Hauteur calée sur la zone réellement visible par LinkedIn sur le profil
(LinkedIn rogne haut/bas la version 4:1 d'origine), donc upload direct sans crop.
Ouvre ce fichier dans Chrome et clique sur les boutons en haut :
- "Télécharger PDF" → fichier rubis-linkedin-banner.pdf, dimensions exactes
- "Télécharger PNG" → fichier rubis-linkedin-banner.png, prêt à uploader LinkedIn
Zone "safe" pour la photo de profil LinkedIn (overlap bas-gauche) :
~200 × 100 px en bas à gauche → laissée libre dans cette compo.
-->
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Bannière LinkedIn — 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-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;
}
/* Barre d'export — masquée à la capture */
.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;
}
.banner {
width: 1584px;
height: 396px;
background: var(--cream);
position: relative;
display: grid;
grid-template-columns: 1fr auto;
gap: 72px;
padding: 56px 80px;
overflow: hidden;
}
/* Halo rubis discret en arrière-plan */
.banner::before {
content: "";
position: absolute;
top: -200px;
right: -160px;
width: 460px;
height: 460px;
background: radial-gradient(circle, var(--rubis-glow) 0%, transparent 65%);
pointer-events: none;
}
/* Gem géant en watermark, très transparent */
.banner::after {
content: "";
position: absolute;
bottom: -200px;
left: 30%;
width: 320px;
height: 320px;
transform: rotate(45deg);
background: var(--rubis);
opacity: 0.04;
border-radius: 24px;
pointer-events: none;
}
/* ===== Left side : brand + tagline ===== */
/* Décalé de 300px à droite pour éviter le chevauchement avec la photo de profil LinkedIn */
.left {
display: flex;
flex-direction: column;
justify-content: center;
z-index: 1;
padding-left: 300px;
}
.brand-row {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 24px;
}
.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: 42px;
color: var(--ink);
line-height: 1.12;
letter-spacing: -0.026em;
max-width: 580px;
}
.tagline em {
font-style: italic;
color: var(--rubis);
}
.url {
margin-top: 24px;
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: 9px 16px;
border-radius: 6px;
border: 1px solid var(--line);
align-self: flex-start;
}
.url::before {
content: "";
width: 8px;
height: 8px;
background: var(--rubis);
transform: rotate(45deg);
}
/* ===== Right side : dashboard preview ===== */
.right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
z-index: 1;
}
.preview {
width: 100%;
max-width: 520px;
background: white;
border: 1px solid var(--line);
border-radius: 14px;
padding: 22px 26px;
box-shadow:
0 14px 32px -14px rgba(26, 20, 16, 0.18),
0 4px 10px -4px rgba(26, 20, 16, 0.08);
}
/* Hero rubis */
.rubis-hero {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--line);
}
.rubis-hero .gem-big {
width: 48px;
height: 48px;
filter: drop-shadow(0 4px 8px rgba(159, 18, 57, 0.3));
flex-shrink: 0;
}
.rubis-count {
font-family: "Bricolage Grotesque", sans-serif;
font-weight: 700;
font-size: 30px;
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 */
.kpis {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 22px;
margin-top: 16px;
}
.kpi-label {
font-size: 13px;
color: var(--ink-3);
text-transform: uppercase;
letter-spacing: 0.06em;
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: 4px;
font-variant-numeric: tabular-nums;
}
.kpi-delta {
font-size: 13px;
color: var(--rubis);
font-weight: 500;
margin-top: 3px;
}
/* Activity feed */
.activity {
margin-top: 14px;
padding-top: 14px;
border-top: 1px dashed var(--line);
font-size: 14px;
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: 13px;
font-variant-numeric: tabular-nums;
}
</style>
</head>
<body>
<div class="controls">
<h2>Bannière LinkedIn — 1584 × 396</h2>
<button id="exportPng2x" class="btn btn-primary">🖼️ PNG 2× (3168×792) — recommandé</button>
<button id="exportPng4x" class="btn btn-secondary">🖼️ PNG 4× (6336×1584)</button>
<button id="exportPng1x" class="btn btn-secondary">🖼️ PNG 1× (1584×396)</button>
<button id="exportPdf" class="btn btn-secondary">📄 PDF</button>
<span class="hint">LinkedIn re-encode en JPEG basse qualité. Upload <b>2×</b> ou <b>4×</b> → leur downscale serveur préserve mieux le texte qu'un upload 1×.</span>
</div>
<div class="banner">
<!-- ========== LEFT : brand + tagline ========== -->
<div class="left">
<div class="brand-row">
<svg class="gem-svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- 4 facettes avec opacités progressives + contour -->
<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> pendant que vous travaillez.
</h1>
<div class="url">rubis.pro</div>
</div>
<!-- ========== RIGHT : mini dashboard ========== -->
<div class="right">
<div class="preview">
<div class="rubis-hero">
<svg class="gem-big" viewBox="0 0 100 100">
<!-- 4 facettes avec opacités progressives + contour -->
<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>
<!-- Libs CDN pour export PDF + PNG -->
<script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.2/dist/jspdf.umd.min.js"></script>
<script>
const banner = document.querySelector(".banner");
const btnPng1x = document.getElementById("exportPng1x");
const btnPng2x = document.getElementById("exportPng2x");
const btnPng4x = document.getElementById("exportPng4x");
const btnPdf = document.getElementById("exportPdf");
// Dimensions logiques de la bannière (CSS)
const BASE_W = 1584;
const BASE_H = 396;
/**
* Pourquoi rendre plus grand que 1584×396 ?
* LinkedIn re-encode tous les uploads en JPEG basse qualité après
* downscale serveur. Si on upload pile 1584×396, leur réencodage
* détruit le texte fin. Si on upload 2× ou 4× plus grand, leur
* downscale serveur a plus de pixels d'origine et le texte reste net.
*
* Recommandation 2026 (testée par la communauté) : uploader en 2× ou 3×
* pour les bannières LinkedIn. Le PNG résultant fait 1-3 MB, largement
* sous la limite 8 MB de LinkedIn.
*/
async function renderAtScale(scale) {
await document.fonts.ready;
// On laisse html-to-image rendre directement à la résolution voulue
// (sans downscale canvas ensuite — le rastériseur du navigateur est
// de meilleure qualité qu'un drawImage downscale).
return await htmlToImage.toPng(banner, {
width: BASE_W,
height: BASE_H,
canvasWidth: BASE_W * scale,
canvasHeight: BASE_H * scale,
pixelRatio: scale,
backgroundColor: "#FAF7F2",
cacheBust: true,
});
}
function makeExporter(btn, scale, label) {
return async function () {
btn.disabled = true;
const original = btn.textContent;
btn.textContent = "⏳ Génération…";
try {
const dataUrl = await renderAtScale(scale);
const link = document.createElement("a");
link.download = `rubis-linkedin-banner-${BASE_W * scale}x${BASE_H * scale}.png`;
link.href = dataUrl;
link.click();
btn.textContent = `${label}`;
setTimeout(() => {
btn.textContent = original;
btn.disabled = false;
}, 2200);
} catch (err) {
console.error("Export PNG :", err);
alert("Erreur export PNG : " + err.message);
btn.disabled = false;
btn.textContent = original;
}
};
}
btnPng1x.addEventListener("click", makeExporter(btnPng1x, 1, "1× téléchargé"));
btnPng2x.addEventListener("click", makeExporter(btnPng2x, 2, "2× téléchargé"));
btnPng4x.addEventListener("click", makeExporter(btnPng4x, 4, "4× téléchargé"));
btnPdf.addEventListener("click", async function () {
btnPdf.disabled = true;
btnPdf.textContent = "⏳ Génération…";
try {
// Pour le PDF on rend en 2× pour avoir une image bien nette dedans
const dataUrl = await renderAtScale(2);
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({
orientation: "landscape",
unit: "px",
format: [BASE_W, BASE_H],
hotfixes: ["px_scaling"],
});
pdf.addImage(dataUrl, "PNG", 0, 0, BASE_W, BASE_H);
pdf.save(`rubis-linkedin-banner-${BASE_W}x${BASE_H}.pdf`);
btnPdf.textContent = "✓ PDF téléchargé";
setTimeout(() => {
btnPdf.textContent = "📄 PDF";
btnPdf.disabled = false;
}, 2200);
} catch (err) {
console.error("Export PDF :", err);
alert("Erreur export PDF : " + err.message);
btnPdf.disabled = false;
btnPdf.textContent = "📄 PDF";
}
});
</script>
</body>
</html>