Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/assets/social-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions overrides/main.html
Original file line number Diff line number Diff line change
@@ -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 <title> 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 %}
99 changes: 99 additions & 0 deletions tools/site/make-social-preview.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down