feat: improve OG image and meta tags for social sharing

- Resize OG image from ~676x216 to 1200x630 (recommended size)
- Add "rob thijssen" headline text overlay to the OG image
- Center the contribution graph within the canvas
- Expand og:title to 55 chars and og:description to 148 chars
  to meet social platform optimal lengths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 16:08:29 +03:00
parent 22c80fd7af
commit a57682e610
2 changed files with 39 additions and 21 deletions

View File

@@ -227,13 +227,17 @@ fn render_contributions_png(
let count_map: HashMap<NaiveDate, i64> = counts.iter().map(|d| (d.date, d.count)).collect(); let count_map: HashMap<NaiveDate, i64> = counts.iter().map(|d| (d.date, d.count)).collect();
// OG image canvas: 1200x630
let og_w = 1200_f64;
let og_h = 630_f64;
let bg = "#2c3e50";
let cell = 10_f64; let cell = 10_f64;
let gap = 2_f64; let gap = 2_f64;
let step = cell + gap; let step = cell + gap;
let radius = cell / 2.0; let radius = cell / 2.0;
let year_label_w = 40_f64; let year_label_w = 40_f64;
let max_cols = 53; let max_cols = 53;
let bg = "#2c3e50";
let colors = ["rgba(255,255,255,0.05)", "#0e4429", "#006d32", "#26a641", "#39d353"]; let colors = ["rgba(255,255,255,0.05)", "#0e4429", "#006d32", "#26a641", "#39d353"];
@@ -253,7 +257,6 @@ fn render_contributions_png(
} else { } else {
NaiveDate::from_ymd_opt(yr, 12, 31).unwrap() NaiveDate::from_ymd_opt(yr, 12, 31).unwrap()
}; };
// Align to preceding Sunday (weekday 6 = Sunday in chrono's Mon=0 scheme)
let offset = year_start.weekday().num_days_from_sunday(); let offset = year_start.weekday().num_days_from_sunday();
let mut cursor = year_start - chrono::Duration::days(offset as i64); let mut cursor = year_start - chrono::Duration::days(offset as i64);
@@ -294,35 +297,53 @@ fn render_contributions_png(
}; };
let n_rows = rows.len(); let n_rows = rows.len();
let title_h = 28_f64; let graph_w = year_label_w + (max_cols as f64) * step;
let svg_w = year_label_w + (max_cols as f64) * step; let graph_h = (n_rows as f64) * step;
let svg_h = title_h + (n_rows as f64) * step + 8.0;
let total: i64 = counts.iter().map(|d| d.count).sum(); let total: i64 = counts.iter().map(|d| d.count).sum();
let mut svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{svg_w}" height="{svg_h}" viewBox="0 0 {svg_w} {svg_h}"><rect width="100%" height="100%" fill="{bg}"/>"#,
);
let repo_text = if repo_count > 0 { let repo_text = if repo_count > 0 {
format!(" in {repo_count} repositories") format!(" in {repo_count} repositories")
} else { } else {
String::new() String::new()
}; };
// Center the graph within the 1200x630 canvas
let headline_h = 80_f64;
let subtitle_h = 30_f64;
let content_h = headline_h + subtitle_h + graph_h;
let offset_x = (og_w - graph_w) / 2.0;
let offset_y = (og_h - content_h) / 2.0;
let mut svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{og_w}" height="{og_h}" viewBox="0 0 {og_w} {og_h}"><rect width="100%" height="100%" fill="{bg}"/>"#,
);
// Headline
svg.push_str(&format!( svg.push_str(&format!(
r##"<text x="{x}" y="18" fill="#ecf0f1" font-family="sans-serif" font-size="12" opacity="0.6">{total} contributions since {from}{repo_text}</text>"##, r##"<text x="{x}" y="{y}" fill="#ecf0f1" font-family="sans-serif" font-size="36" font-weight="bold">rob thijssen</text>"##,
x = year_label_w, x = offset_x + year_label_w,
y = offset_y + 40.0,
)); ));
// Subtitle
svg.push_str(&format!(
r##"<text x="{x}" y="{y}" fill="#ecf0f1" font-family="sans-serif" font-size="14" opacity="0.6">{total} contributions since {from}{repo_text}</text>"##,
x = offset_x + year_label_w,
y = offset_y + headline_h + 14.0,
));
let graph_y = offset_y + headline_h + subtitle_h;
for (row_idx, row) in rows.iter().enumerate() { for (row_idx, row) in rows.iter().enumerate() {
let y_base = title_h + (row_idx as f64) * step; let y_base = graph_y + (row_idx as f64) * step;
svg.push_str(&format!( svg.push_str(&format!(
r##"<text x="{x}" y="{y}" text-anchor="end" dominant-baseline="central" fill="#ecf0f1" font-family="sans-serif" font-size="9" opacity="0.6">{yr}</text>"##, r##"<text x="{x}" y="{y}" text-anchor="end" dominant-baseline="central" fill="#ecf0f1" font-family="sans-serif" font-size="9" opacity="0.6">{yr}</text>"##,
x = year_label_w - 4.0, x = offset_x + year_label_w - 4.0,
y = y_base + radius, y = y_base + radius,
yr = row.year, yr = row.year,
)); ));
for (col, (_, _, count)) in row.weeks.iter().enumerate() { for (col, (_, _, count)) in row.weeks.iter().enumerate() {
let cx = year_label_w + (col as f64) * step + radius; let cx = offset_x + year_label_w + (col as f64) * step + radius;
let cy = y_base + radius; let cy = y_base + radius;
let fill = color_for(*count); let fill = color_for(*count);
svg.push_str(&format!( svg.push_str(&format!(
@@ -334,15 +355,12 @@ fn render_contributions_png(
svg.push_str("</svg>"); svg.push_str("</svg>");
// Rasterize with resvg // Rasterize at 1200x630
let tree = resvg::usvg::Tree::from_str(&svg, &resvg::usvg::Options::default()) let tree = resvg::usvg::Tree::from_str(&svg, &resvg::usvg::Options::default())
.map_err(|e| format!("svg parse: {e}"))?; .map_err(|e| format!("svg parse: {e}"))?;
let size = tree.size();
let w = size.width().ceil() as u32;
let h = size.height().ceil() as u32;
let mut pixmap = let mut pixmap =
resvg::tiny_skia::Pixmap::new(w, h).ok_or_else(|| "pixmap alloc failed".to_string())?; resvg::tiny_skia::Pixmap::new(og_w as u32, og_h as u32).ok_or_else(|| "pixmap alloc failed".to_string())?;
resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut()); resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>rob.tn</title> <title>rob.tn</title>
<meta property="og:title" content="rob thijssen" /> <meta property="og:title" content="rob thijssen — developer activity and contribution history" />
<meta property="og:description" content="contribution history across github, gitea, and mozilla hg" /> <meta property="og:description" content="a timeline of open source contributions across github, gitea, and mozilla hg — ranked projects, language trends, and commit activity since 2012." />
<meta property="og:image" content="https://rob.tn/api/v1/og/contributions.png" /> <meta property="og:image" content="https://rob.tn/api/v1/og/contributions.png" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />