diff --git a/Justfile b/Justfile index c92ba01..a2d4bdd 100644 --- a/Justfile +++ b/Justfile @@ -10,3 +10,9 @@ lint-ci: check-planning test: uv run pytest + +# Regenerate the brand kit and copy the subset the site serves into docs/assets. +sync-assets: + uv run python -m brand.build.render + cp brand/org/favicon.svg brand/org/mark.svg brand/org/wordmark.svg brand/org/wordmark-dark.svg docs/assets/ + cp brand/org/social-card-green.png docs/assets/ diff --git a/architecture/site-branding.md b/architecture/site-branding.md new file mode 100644 index 0000000..f9d337d --- /dev/null +++ b/architecture/site-branding.md @@ -0,0 +1,49 @@ +# Site branding assets + +How the org site (`modern-python.org`) gets its logo, favicon, hero wordmark, and +social card. The canonical brand kit is generated by `brand/build/`; the site +serves a copied subset under `docs/assets/`. + +## Pipeline + +1. `brand/build/` is the source of truth. `uv run python -m brand.build.render` + draws every asset from `brand/build/geometry.py` (vector geometry; text is + outlined to paths via the bundled Jost TTF, so nothing depends on a font at + serve time) into `brand/org/`. +2. `just sync-assets` runs the render, then copies the site subset into + `docs/assets/`: `favicon.svg`, `mark.svg`, `wordmark.svg`, `wordmark-dark.svg`, + and `social-card-green.png`. (The site can only serve files under `docs/`, so + the copies are intentional duplication, not drift — regenerate via the recipe.) +3. MkDocs Material consumes them (see wiring below). + +## Geometry building blocks (`brand/build/geometry.py`) + +- `icon()` — full-bleed square mark (favicon/avatar/apple-touch), with background. +- `mark()` — the same chevron mark with **no background**, for the site header. +- `lockup_body()` — the `MODERN`/`PYTHON` crop-mark wordmark, drawn in a 540x250 + space, returned as bare markup for embedding. +- `wordmark()` — `lockup_body` wrapped in a tight transparent viewBox + (`118 32 304 184`) for standalone use in the hero. +- `social_card()` / `social_square()` — the wordmark composited onto a background. + +Two color roles flow through every function: `struct` (the cream/green-ink +structure) and `gold` (the accent). The site uses two wordmark variants: +green-ink + gold-light (`wordmark.svg`, light pages) and cream + gold-dark +(`wordmark-dark.svg`, dark pages and the green header). + +## Site wiring + +- **Header** (`mkdocs.yml` `theme.logo: assets/mark.svg`) — the chevron mark, with + Material's default "Modern Python" site title (Roboto) and search beside it. +- **Favicon** (`mkdocs.yml` `theme.favicon: assets/favicon.svg`). +- **Hero** (`docs/index.md`) — both wordmark variants sit inside the page `

` + (`.mp-wordmark`); `extra.css` shows the light variant by default and swaps to + the dark variant under `[data-md-color-scheme="slate"]`. The `

` is load- + bearing: Material injects a fallback `

{{ page.title }}

` (which read + "Home" on the index) only when the rendered content has no `

`, so wrapping + the wordmark in one suppresses it. +- **Social card** (`overrides/main.html`) — the `og:image` / `twitter:image` + meta points at `assets/social-card-green.png` (the green card). + +The brand palette and token roles live in `brand/build/tokens.py` and +`brand/README.md`. diff --git a/brand/build/geometry.py b/brand/build/geometry.py index 7c4f48f..0801926 100644 --- a/brand/build/geometry.py +++ b/brand/build/geometry.py @@ -52,6 +52,24 @@ def lockup_body(*, struct: str, gold: str) -> str: return crops + modern + python +def wordmark(*, struct: str, gold: str) -> str: + """Standalone two-color MODERN/PYTHON wordmark for the site hero. Wraps + `lockup_body` (drawn in a 540x250 space) in a tight viewBox centered on the + content, with no background — transparent so it sits on any page surface.""" + return ( + '' + + lockup_body(struct=struct, gold=gold) + + "" + ) + + +def mark(*, struct: str, gold: str) -> str: + """The chevron mark on its own (no background) for the site header logo — + same glyph as `icon`, minus the full-bleed background rect.""" + return _SVG_OPEN.format(w=100, h=100) + _icon_mark(struct, gold) + "" + + def social_card(*, bg: str, struct: str, gold: str, url_color: str) -> str: body = lockup_body(struct=struct, gold=gold) url, _ = outline_text("modern-python.org", 34, x=640, baseline_y=575, diff --git a/brand/build/render.py b/brand/build/render.py index da60416..29244b7 100644 --- a/brand/build/render.py +++ b/brand/build/render.py @@ -51,6 +51,13 @@ def render() -> None: g.icon_circle(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK)) export_png(ORG / "avatar-circle.svg", ORG / "avatar-circle-1024.png", width=1024, height=1024) + # Site logos — transparent, no background. + # wordmark (hero): two-color lockup, light + dark variants + # mark (header): chevron mark in cream/gold-dark for the green header bar + _write(ORG / "wordmark.svg", g.wordmark(struct=t.GREEN_INK, gold=t.GOLD_LIGHT)) + _write(ORG / "wordmark-dark.svg", g.wordmark(struct=t.CREAM, gold=t.GOLD_DARK)) + _write(ORG / "mark.svg", g.mark(struct=t.CREAM, gold=t.GOLD_DARK)) + # Social cards — cream (primary) + green (alternate). _write(ORG / "social-card.svg", g.social_card(bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT, url_color=t.GOLD_LIGHT)) diff --git a/brand/org/mark.svg b/brand/org/mark.svg new file mode 100644 index 0000000..c31e2ff --- /dev/null +++ b/brand/org/mark.svg @@ -0,0 +1 @@ + diff --git a/brand/org/wordmark-dark.svg b/brand/org/wordmark-dark.svg new file mode 100644 index 0000000..495d9f5 --- /dev/null +++ b/brand/org/wordmark-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/org/wordmark.svg b/brand/org/wordmark.svg new file mode 100644 index 0000000..d3c2846 --- /dev/null +++ b/brand/org/wordmark.svg @@ -0,0 +1 @@ + diff --git a/docs/assets/modern-python-favicon.svg b/docs/assets/favicon.svg similarity index 100% rename from docs/assets/modern-python-favicon.svg rename to docs/assets/favicon.svg diff --git a/docs/assets/mark.svg b/docs/assets/mark.svg new file mode 100644 index 0000000..c31e2ff --- /dev/null +++ b/docs/assets/mark.svg @@ -0,0 +1 @@ + diff --git a/docs/assets/modern-python-mark.svg b/docs/assets/modern-python-mark.svg deleted file mode 100644 index e441c2a..0000000 --- a/docs/assets/modern-python-mark.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/assets/modern-python-white.svg b/docs/assets/modern-python-white.svg deleted file mode 100644 index 77b32d8..0000000 --- a/docs/assets/modern-python-white.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/assets/modern-python.png b/docs/assets/modern-python.png deleted file mode 100644 index d7201c9..0000000 Binary files a/docs/assets/modern-python.png and /dev/null differ diff --git a/docs/assets/modern-python.svg b/docs/assets/modern-python.svg deleted file mode 100644 index 7078a37..0000000 --- a/docs/assets/modern-python.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/assets/social-card-green.png b/docs/assets/social-card-green.png new file mode 100644 index 0000000..37eef6c Binary files /dev/null and b/docs/assets/social-card-green.png differ diff --git a/docs/assets/social-card.png b/docs/assets/social-card.png deleted file mode 100644 index 5b64619..0000000 Binary files a/docs/assets/social-card.png and /dev/null differ diff --git a/docs/assets/wordmark-dark.svg b/docs/assets/wordmark-dark.svg new file mode 100644 index 0000000..495d9f5 --- /dev/null +++ b/docs/assets/wordmark-dark.svg @@ -0,0 +1 @@ + diff --git a/docs/assets/wordmark.svg b/docs/assets/wordmark.svg new file mode 100644 index 0000000..d3c2846 --- /dev/null +++ b/docs/assets/wordmark.svg @@ -0,0 +1 @@ + diff --git a/docs/index.md b/docs/index.md index 922c04f..1c5d487 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ --- +title: Modern Python hide: - navigation - toc @@ -6,16 +7,15 @@ hide:
- +

+ + +

Open-source templates and libraries for building production-ready Python applications — web services, microservices, and the dependency injection that wires them together. -

Built with uv, -ruff, and -ty.

-
@@ -116,3 +116,7 @@ catalog below. - [`db-retry`](https://github.com/modern-python/db-retry) — retry helpers for database operations. - [`eof-fixer`](https://github.com/modern-python/eof-fixer) — automatically fix newlines at the end of files. - [`semvertag`](https://github.com/modern-python/semvertag) — auto-tag your GitHub/GitLab repo with semantic version tags from CI. + +

Built with uv, +ruff, and +ty.

diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index adf054c..d410c72 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -22,15 +22,26 @@ text-align: center; margin: 2rem 0 3rem; } +/* The wordmark is the page

; strip the default heading chrome. */ +.mp-hero .mp-wordmark { + margin: 0; + font-size: 0; /* collapse whitespace between the stacked variants */ + line-height: 0; +} .mp-hero .mp-logo { max-width: 420px; width: 70%; height: auto; - color: var(--md-primary-fg-color); /* brand green on the light page */ } -/* Lighter green in dark mode so the wordmark keeps contrast on the dark bg */ -[data-md-color-scheme="slate"] .mp-hero .mp-logo { - color: #7fb79f; +/* Two-color wordmark: light variant by default, cream variant in dark mode. */ +.mp-hero .mp-logo--dark { + display: none; +} +[data-md-color-scheme="slate"] .mp-hero .mp-logo--light { + display: none; +} +[data-md-color-scheme="slate"] .mp-hero .mp-logo--dark { + display: inline; } .mp-hero .mp-tagline { color: var(--md-default-fg-color--light); diff --git a/mkdocs.yml b/mkdocs.yml index 517ff15..5ffe36f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,8 +11,8 @@ exclude_docs: | theme: name: material custom_dir: overrides - logo: assets/modern-python-white.svg - favicon: assets/modern-python-favicon.svg + logo: assets/mark.svg + favicon: assets/favicon.svg features: - navigation.instant - navigation.top diff --git a/overrides/main.html b/overrides/main.html index 8f33930..e2214e4 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -1,9 +1,25 @@ {% extends "base.html" %} +{#- The homepage sets a front-matter `title` so the header topic reads + "Modern Python" instead of the nav default "Home". Material's default + htmltitle would then render "Modern Python - Modern Python"; + collapse that to just the site name on the homepage. -#} +{% block htmltitle %} + {%- if page.is_homepage %} + {{ config.site_name }} + {%- elif page.meta and page.meta.title %} + {{ page.meta.title }} - {{ config.site_name }} + {%- elif page.title %} + {{ page.title | striptags }} - {{ config.site_name }} + {%- else %} + {{ config.site_name }} + {%- endif %} +{% endblock %} + {#- Open Graph / Twitter card metadata for modern-python.org. -#} {% block extrahead %} {{ super() }} - {% set card = config.site_url ~ "assets/social-card.png" %} + {% set card = config.site_url ~ "assets/social-card-green.png" %} {%- if page.meta and page.meta.title %} {%- set title = page.meta.title %} {%- elif page.is_homepage %} diff --git a/scripts/gen_logo_svg.py b/scripts/gen_logo_svg.py deleted file mode 100644 index cc78917..0000000 --- a/scripts/gen_logo_svg.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Generate a transparent, vector SVG wordmark for Modern Python. - -Extracts the "MODERN PYTHON" glyph outlines from Futura (present on macOS) as -real vector paths — no font dependency at render time — and frames them with the -brand's corner brackets. Fill is `currentColor` so the same file can be tinted -per theme (green on light surfaces, white on the green header) via CSS/attr. - -Run locally: - - uv run --with fonttools python scripts/gen_logo_svg.py - -Outputs: - docs/assets/modern-python.svg (two-line wordmark + brackets) - docs/assets/modern-python-mark.svg (compact bracket mark, for the favicon) -""" - -import pathlib - -from fontTools.pens.svgPathPen import SVGPathPen -from fontTools.ttLib import TTFont - -FONT_PATH = "/System/Library/Fonts/Supplemental/Futura.ttc" -FONT_NUMBER = 0 # Futura Medium -ASSETS = pathlib.Path(__file__).resolve().parent.parent / "docs" / "assets" - -LINES = ["MODERN", "PYTHON"] -TRACKING = 0.10 # fraction of em added between glyphs -LINE_GAP = 0.18 # fraction of em between the two lines -PAD = 0.34 # padding (em) inside the bracket frame -BRACKET = 0.55 # bracket arm length (em) -STROKE = 0.07 # bracket stroke width (em) - - -def layout_line(font, glyph_set, cmap, text, upm): - """Return (svg_paths, width_em) for one tracked line, baseline at y=0, y-up.""" - paths, x = [], 0.0 - track = TRACKING * upm - for ch in text: - gname = cmap[ord(ch)] - pen = SVGPathPen(glyph_set) - glyph_set[gname].draw(pen) - d = pen.getCommands() - if d: - paths.append(f'') - x += glyph_set[gname].width + track - return paths, (x - track) / upm # drop trailing track - - -def main() -> None: - font = TTFont(FONT_PATH, fontNumber=FONT_NUMBER) - glyph_set = font.getGlyphSet() - cmap = font.getBestCmap() - upm = font["head"].unitsPerEm - cap = font["OS/2"].sCapHeight if hasattr(font["OS/2"], "sCapHeight") else int(0.7 * upm) - cap_em = cap / upm - - # Lay out each line; track widest for centering. - laid = [layout_line(font, glyph_set, cmap, line, upm) for line in LINES] - text_w = max(w for _, w in laid) - - line_h = cap_em + LINE_GAP - text_h = line_h * len(LINES) - LINE_GAP - - # Frame geometry (em units, y-down in final SVG). - frame_w = text_w + 2 * PAD - frame_h = text_h + 2 * PAD - vb_w = frame_w - vb_h = frame_h - - groups = [] - # Each line: flip y-up glyphs into y-down SVG space and position. - for i, (paths, w) in enumerate(laid): - if not paths: - continue - x0 = (frame_w - w) / 2 * upm - baseline = (PAD + cap_em + i * line_h) * upm # y of baseline in y-down em*upm - # translate to (x0, baseline) then scale(1,-1) maps glyph y-up to y-down. - groups.append( - f'{"".join(paths)}' - ) - - # Corner brackets: top-left and bottom-right, stroked (no fill). - sw, arm = STROKE, BRACKET - - def wordmark(color: str) -> str: - tl = ( - f'' - ) - br = ( - f'' - ) - return ( - f'' - f'{"".join(groups)}{tl}{br}\n' - ) - - # currentColor variant — inlined in the homepage hero, themed via CSS. - (ASSETS / "modern-python.svg").write_text(wordmark("currentColor")) - # White variant — used as the header logo (the header sits on brand green). - (ASSETS / "modern-python-white.svg").write_text(wordmark("#ffffff")) - print(f"wrote modern-python.svg + modern-python-white.svg (viewBox 0 0 {vb_w:.2f} {vb_h:.2f})") - - # Compact favicon mark: the two corner brackets only, in a tight square. - # Adapts to the browser/OS theme: brand green on light tabs, white on dark. - m_sw, m_arm = 0.12, 0.62 - paths = ( - f'' - f'' - ) - style = ( - "" - ) - mark = ( - f'' - f'{style}{paths}\n' - ) - (ASSETS / "modern-python-mark.svg").write_text(mark) - print("wrote modern-python-mark.svg") - - -if __name__ == "__main__": - main() diff --git a/scripts/gen_social_card.py b/scripts/gen_social_card.py deleted file mode 100644 index b85221c..0000000 --- a/scripts/gen_social_card.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Generate the Open Graph / social-card image for modern-python.org. - -Layout C: centered wordmark, thin divider, domain — white on brand green. -Run locally (needs the Futura font, present on macOS): - - uv run --with pillow python scripts/gen_social_card.py - -Output: docs/assets/social-card.png (1200x630). -""" - -import math -import pathlib - -from PIL import Image, ImageDraw, ImageFont - -WIDTH, HEIGHT = 1200, 630 -GREEN = (53, 104, 82) # #356852, the brand color sampled from the logo -WHITE = (255, 255, 255) - -FONT_PATH = "/System/Library/Fonts/Supplemental/Futura.ttc" -FONT_INDEX = 0 # Futura Medium - -WORDMARK = "MODERN PYTHON" -DOMAIN = "MODERN-PYTHON.ORG" -OUT = pathlib.Path(__file__).resolve().parent.parent / "docs" / "assets" / "social-card.png" - - -def render_tracked(text: str, font: ImageFont.FreeTypeFont, tracking: float, alpha: int) -> Image.Image: - """Render `text` with manual letter-spacing into a tight RGBA image.""" - advances = [font.getlength(ch) for ch in text] - total_w = sum(advances) + tracking * (len(text) - 1) - bbox = font.getbbox(text) - top, height = bbox[1], bbox[3] - bbox[1] - img = Image.new("RGBA", (math.ceil(total_w), height), (0, 0, 0, 0)) - draw = ImageDraw.Draw(img) - x = 0.0 - for ch, adv in zip(text, advances): - draw.text((x, -top), ch, font=font, fill=(*WHITE, alpha)) - x += adv + tracking - return img - - -def fit_wordmark(max_width: int) -> Image.Image: - """Pick the largest Futura size whose tracked wordmark fits max_width.""" - for size in range(140, 40, -2): - font = ImageFont.truetype(FONT_PATH, size, index=FONT_INDEX) - img = render_tracked(WORDMARK, font, tracking=size * 0.14, alpha=255) - if img.width <= max_width: - return img - raise RuntimeError("wordmark never fit") - - -def main() -> None: - base = Image.new("RGBA", (WIDTH, HEIGHT), (*GREEN, 255)) - - wordmark = fit_wordmark(max_width=1000) - domain_font = ImageFont.truetype(FONT_PATH, 30, index=FONT_INDEX) - domain = render_tracked(DOMAIN, domain_font, tracking=30 * 0.16, alpha=217) # ~85% - - divider_w, divider_h = 360, 3 - gap1, gap2 = 48, 42 - group_h = wordmark.height + gap1 + divider_h + gap2 + domain.height - y = (HEIGHT - group_h) // 2 - - base.alpha_composite(wordmark, ((WIDTH - wordmark.width) // 2, y)) - y += wordmark.height + gap1 - - divider = Image.new("RGBA", (divider_w, divider_h), (*WHITE, 128)) # ~50% - base.alpha_composite(divider, ((WIDTH - divider_w) // 2, y)) - y += divider_h + gap2 - - base.alpha_composite(domain, ((WIDTH - domain.width) // 2, y)) - - OUT.parent.mkdir(parents=True, exist_ok=True) - base.convert("RGB").save(OUT, "PNG") - print(f"wrote {OUT} ({base.width}x{base.height})") - - -if __name__ == "__main__": - main() diff --git a/tests/test_assets.py b/tests/test_assets.py index ea0af6e..4d06c1d 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -47,6 +47,22 @@ def test_render_writes_avatar_circle(): assert (ORG / "avatar-circle-1024.png").read_bytes()[:8] == b"\x89PNG\r\n\x1a\n" +def test_render_writes_site_wordmark_and_mark(): + _render() + light = (ORG / "wordmark.svg").read_text() + assert ET.parse(ORG / "wordmark.svg") is not None + assert "#356852" in light and "#c98a00" in light # green ink + gold-light + dark = (ORG / "wordmark-dark.svg").read_text() + assert "#f4f1e8" in dark and "#f0b528" in dark # cream + gold-dark + for wm in ("wordmark.svg", "wordmark-dark.svg"): + assert "