From eda9028afd1cf8a789cb53e625eca4f41120ddc9 Mon Sep 17 00:00:00 2001 From: Adventure Agent Date: Fri, 16 Oct 2026 17:34:33 +0000 Subject: [PATCH] docs(site): add Open Graph, Twitter card and JSON-LD structured data Closes the social-preview and structured-data part of #17. - overrides/main.html: extrahead block emitting og:*, twitter:card and JSON-LD (SoftwareSourceCode on the front page, TechArticle on guide pages) from the same title/description the theme already renders, plus the canonical URL and an absolute og:image. - docs/assets/social-preview.png: 1200x630 card drawn from the site's own logo (hash-bit grid in the site accent #2a78d6) by a portable Pillow script, tools/site/make-social-preview.py. - zensical.toml: theme.custom_dir = overrides. Verified with a strict build: every one of the 25 pages emits og:image, twitter:card and a per-page og:title; the front page carries SoftwareSourceCode JSON-LD and guide pages TechArticle JSON-LD. Co-Authored-By: Paperclip --- docs/assets/social-preview.png | Bin 0 -> 24978 bytes overrides/main.html | 65 ++++++++++++++++++++ tools/site/make-social-preview.py | 99 ++++++++++++++++++++++++++++++ zensical.toml | 1 + 4 files changed, 165 insertions(+) create mode 100644 docs/assets/social-preview.png create mode 100644 overrides/main.html create mode 100644 tools/site/make-social-preview.py diff --git a/docs/assets/social-preview.png b/docs/assets/social-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..ec9f67768368841949599b666b7ba7c467abf799 GIT binary patch literal 24978 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 P|GY~ybv1I;EuQ`x@DPtQ literal 0 HcmV?d00001 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 = [