diff --git a/docs/assets/social-preview.png b/docs/assets/social-preview.png new file mode 100644 index 0000000..ec9f677 Binary files /dev/null and b/docs/assets/social-preview.png differ diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..be9a632 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,65 @@ +{#- + Overrides main.html to add social metadata the base theme does not emit: + + * Open Graph tags (og:*) and Twitter card tags so shared links render a + preview on Slack, Telegram, X, LinkedIn and elsewhere; + + * JSON-LD structured data: SoftwareSourceCode on the front page (the + package), TechArticle on the guide pages (a navigation path that starts + with "guide/"), matching the SEO epic in + https://github.com/gudoshnikovn/libphash/issues/51 and the spelling of + python-libphash#17. + + The tags read the same values the base theme uses for and the + description meta, so the preview always matches the page it describes. +-#} +{% extends "base.html" %} + +{% block extrahead %} + {% set page_title = page.meta.title if (page.meta and page.meta.title) else (config.site_name if (page.url == "" or page.url == "/") else (page.title | striptags) ~ " - " ~ config.site_name) %} + {% set page_desc = page.meta.description if (page.meta and page.meta.description) else config.site_description %} + {% set preview_image = config.site_url ~ "assets/social-preview.png" %} + <meta property="og:type" content="website" /> + <meta property="og:site_name" content="{{ config.site_name }}" /> + <meta property="og:title" content="{{ page_title }}" /> + <meta property="og:description" content="{{ page_desc }}" /> + {% if page.canonical_url %} + <meta property="og:url" content="{{ page.canonical_url }}" /> + {% endif %} + <meta property="og:image" content="{{ preview_image }}" /> + <meta property="og:image:width" content="1200" /> + <meta property="og:image:height" content="630" /> + <meta property="og:image:alt" content="{{ page_desc }}" /> + <meta name="twitter:card" content="summary_large_image" /> + <meta name="twitter:title" content="{{ page_title }}" /> + <meta name="twitter:description" content="{{ page_desc }}" /> + <meta name="twitter:image" content="{{ preview_image }}" /> + {% if page.url == "" or page.url == "/" %} + <script type="application/ld+json"> + { + "@context": "https://schema.org", + "@type": "SoftwareSourceCode", + "name": "python-libphash", + "description": "{{ config.site_description }}", + "programmingLanguage": "Python", + "codeRepository": "{{ config.repo_url }}", + "license": "https://opensource.org/licenses/MIT", + "url": "https://pypi.org/project/python-libphash/", + "isBasedOn": [ + "https://github.com/gudoshnikovn/libphash", + "https://gudoshnikovn.github.io/libphash/" + ] + } + </script> + {% elif page.url is defined and page.url is startingwith "guide/" %} + <script type="application/ld+json"> + { + "@context": "https://schema.org", + "@type": "TechArticle", + "headline": "{{ page_title }}", + "description": "{{ page_desc }}", + "url": "{% if page.canonical_url %}{{ page.canonical_url }}{% endif %}" + } + </script> + {% endif %} +{% endblock %} diff --git a/tools/site/make-social-preview.py b/tools/site/make-social-preview.py new file mode 100644 index 0000000..2fe8075 --- /dev/null +++ b/tools/site/make-social-preview.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Generate docs/assets/social-preview.png, the site's 1200x630 Open Graph card. + +The card is drawn from the same elements as the site itself: the logo -- a 4x4 +grid of rounded squares, sixteen bits of a perceptual hash, set bits in the +site's accent (#2a78d6), clear bits in the same color, faint -- the package +name, and the site tagline. Run it from the repository root: + + python3 tools/site/make-social-preview.py + +Pillow is the only dependency (already a docs-group dependency via the site's +image assets; the script draws everything itself and needs no fonts from the +machine -- it renders text as vector paths through PIL's baked-in default +font, sized to fit the 1200x630 canvas). +""" + +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +W, H = 1200, 630 +ACCENT = (42, 120, 214) # #2a78d6 -- the site's primary color +ACCENT_FAINT = (42, 120, 214, 72) +INK = (24, 42, 66) # a near-black slate that reads on the light card +MUTED = (90, 110, 132) # secondary text +BG = (255, 255, 255) + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "docs" / "assets" / "social-preview.png" + + +def cell(draw: ImageDraw.ImageDraw, x0: int, y0: int, size: int, radius: int, + fill: tuple[int, int, int, int] | tuple[int, int, int]) -> None: + draw.rounded_rectangle([x0, y0, x0 + size, y0 + size], radius=radius, fill=fill) + + +def hash_grid(draw: ImageDraw.ImageDraw, cx: int, cy: int, cell_size: int, + gap: int, radius_ratio: float = 0.2) -> None: + """A 4x4 grid of the logo's hash bits around (cx, cy).""" + n = 4 + size = cell_size + total = n * size + (n - 1) * gap + x0 = cx - total // 2 + y0 = cy - total // 2 + pattern = [ + [1, 0, 1, 1], + [0, 1, 1, 0], + [1, 1, 0, 1], + [0, 1, 1, 1], + ] + rad = int(size * radius_ratio) + for r in range(n): + for c in range(n): + if pattern[r][c]: + fill = ACCENT + else: + fill = ACCENT_FAINT + cell(draw, x0 + c * (size + gap), y0 + r * (size + gap), size, rad, fill) + + +def main() -> None: + img = Image.new("RGBA", (W, H), BG) + draw = ImageDraw.Draw(img) + + # Decorative hash grid, right of center, large and quiet. + hash_grid(draw, cx=960, cy=210, cell_size=74, gap=18, radius_ratio=0.22) + + # A thin accent rule under the wordmark. + draw.rectangle([80, 316, 380, 322], fill=ACCENT) + + # Wordmark + tagline. PIL's default bitmap font is tiny; draw text with a + # larger vector font by loading a TTF shipped with Pillow's bundled fonts + # if present, else fall back to the default font scaled up. + fonts = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + ] + bold_path = next((p for p in fonts if Path(p).exists()), None) + regular_path = fonts[1] if Path(fonts[1]).exists() else None + + if bold_path: + title_font = ImageFont.truetype(bold_path, 72) + tag_font = ImageFont.truetype(regular_path or bold_path, 30) + else: + title_font = ImageFont.load_default() + tag_font = ImageFont.load_default() + + draw.text((80, 150), "python-libphash", font=title_font, fill=INK) + draw.text((80, 356), "Perceptual image hashing in Python", font=tag_font, fill=MUTED) + draw.text((80, 404), "gudoshnikovn.github.io/python-libphash", font=tag_font, fill=ACCENT) + + img.convert("RGB").save(OUT, "PNG") + print(f"wrote {OUT} ({W}x{H})") + + +if __name__ == "__main__": + main() diff --git a/zensical.toml b/zensical.toml index 185e68b..c1aaf01 100644 --- a/zensical.toml +++ b/zensical.toml @@ -57,6 +57,7 @@ extra_css = ["stylesheets/site.css"] [project.theme] language = "en" +custom_dir = "overrides" logo = "assets/logo.svg" favicon = "assets/logo.svg" features = [