Skip to content

Commit 50abd98

Browse files
lesnik512claude
andauthored
feat: add social card image (Open Graph / Twitter) (#3)
Layout C: centered wordmark + divider + domain, white on brand green, 1200x630. Generated by scripts/gen_social_card.py and wired in via an overrides/main.html extrahead block (og:image, twitter:summary_large_image). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent eea69cb commit 50abd98

4 files changed

Lines changed: 94 additions & 0 deletions

File tree

docs/assets/social-card.png

21.6 KB
Loading

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ exclude_docs: |
1111
1212
theme:
1313
name: material
14+
custom_dir: overrides
1415
logo: assets/modern-python.png
1516
favicon: assets/modern-python.png
1617
features:

overrides/main.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% extends "base.html" %}
2+
3+
{#- Static social card (Open Graph / Twitter) for modern-python.org. -#}
4+
{% block extrahead %}
5+
{{ super() }}
6+
{% set card = config.site_url ~ "assets/social-card.png" %}
7+
<meta property="og:image" content="{{ card }}">
8+
<meta property="og:image:type" content="image/png">
9+
<meta property="og:image:width" content="1200">
10+
<meta property="og:image:height" content="630">
11+
<meta name="twitter:card" content="summary_large_image">
12+
<meta name="twitter:image" content="{{ card }}">
13+
{% endblock %}

scripts/gen_social_card.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Generate the Open Graph / social-card image for modern-python.org.
2+
3+
Layout C: centered wordmark, thin divider, domain — white on brand green.
4+
Run locally (needs the Futura font, present on macOS):
5+
6+
uv run --with pillow python scripts/gen_social_card.py
7+
8+
Output: docs/assets/social-card.png (1200x630).
9+
"""
10+
11+
import math
12+
import pathlib
13+
14+
from PIL import Image, ImageDraw, ImageFont
15+
16+
WIDTH, HEIGHT = 1200, 630
17+
GREEN = (53, 104, 82) # #356852, the brand color sampled from the logo
18+
WHITE = (255, 255, 255)
19+
20+
FONT_PATH = "/System/Library/Fonts/Supplemental/Futura.ttc"
21+
FONT_INDEX = 0 # Futura Medium
22+
23+
WORDMARK = "MODERN PYTHON"
24+
DOMAIN = "MODERN-PYTHON.ORG"
25+
OUT = pathlib.Path(__file__).resolve().parent.parent / "docs" / "assets" / "social-card.png"
26+
27+
28+
def render_tracked(text: str, font: ImageFont.FreeTypeFont, tracking: float, alpha: int) -> Image.Image:
29+
"""Render `text` with manual letter-spacing into a tight RGBA image."""
30+
advances = [font.getlength(ch) for ch in text]
31+
total_w = sum(advances) + tracking * (len(text) - 1)
32+
bbox = font.getbbox(text)
33+
top, height = bbox[1], bbox[3] - bbox[1]
34+
img = Image.new("RGBA", (math.ceil(total_w), height), (0, 0, 0, 0))
35+
draw = ImageDraw.Draw(img)
36+
x = 0.0
37+
for ch, adv in zip(text, advances):
38+
draw.text((x, -top), ch, font=font, fill=(*WHITE, alpha))
39+
x += adv + tracking
40+
return img
41+
42+
43+
def fit_wordmark(max_width: int) -> Image.Image:
44+
"""Pick the largest Futura size whose tracked wordmark fits max_width."""
45+
for size in range(140, 40, -2):
46+
font = ImageFont.truetype(FONT_PATH, size, index=FONT_INDEX)
47+
img = render_tracked(WORDMARK, font, tracking=size * 0.14, alpha=255)
48+
if img.width <= max_width:
49+
return img
50+
raise RuntimeError("wordmark never fit")
51+
52+
53+
def main() -> None:
54+
base = Image.new("RGBA", (WIDTH, HEIGHT), (*GREEN, 255))
55+
56+
wordmark = fit_wordmark(max_width=1000)
57+
domain_font = ImageFont.truetype(FONT_PATH, 30, index=FONT_INDEX)
58+
domain = render_tracked(DOMAIN, domain_font, tracking=30 * 0.16, alpha=217) # ~85%
59+
60+
divider_w, divider_h = 360, 3
61+
gap1, gap2 = 48, 42
62+
group_h = wordmark.height + gap1 + divider_h + gap2 + domain.height
63+
y = (HEIGHT - group_h) // 2
64+
65+
base.alpha_composite(wordmark, ((WIDTH - wordmark.width) // 2, y))
66+
y += wordmark.height + gap1
67+
68+
divider = Image.new("RGBA", (divider_w, divider_h), (*WHITE, 128)) # ~50%
69+
base.alpha_composite(divider, ((WIDTH - divider_w) // 2, y))
70+
y += divider_h + gap2
71+
72+
base.alpha_composite(domain, ((WIDTH - domain.width) // 2, y))
73+
74+
OUT.parent.mkdir(parents=True, exist_ok=True)
75+
base.convert("RGB").save(OUT, "PNG")
76+
print(f"wrote {OUT} ({base.width}x{base.height})")
77+
78+
79+
if __name__ == "__main__":
80+
main()

0 commit comments

Comments
 (0)