docs(og): add 1920×1080 cover art set + wire og-01 as og:image#132
docs(og): add 1920×1080 cover art set + wire og-01 as og:image#132
Conversation
Add seven OG / YouTube-thumbnail compositions under `docs/img/og/`,
all rendered from a single gallery page (`docs/og.html`) so they share
the landing page's design system (Fraunces × Spectral × JetBrains Mono ·
paper / ink / vermilion / mora-orange / mora-teal · halftone, washi,
phoneme bars, chop seal):
- og-01 — Overture (paper editorial cover, AppIcon chop, yokai stack)
- og-02 — Substitution slab (dark phoneme math: b←v, r←l, s←th)
- og-03 — The Numbers (5 / 100+ / 25.84 / 0)
- og-04 — Pull quote ("I built Mora for one kid I love." + AppIcon chop)
- og-05 — Yokai triumvirate (three mentor cards on washi)
- og-06 — Terminal (dev/code aesthetic with live phoneme posterior)
- og-01-dyslexic — same as og-01 but the "Mora." wordmark is set in
OpenDyslexic, the dyslexia-friendly typeface this product is built for
Wire og-01.png into the landing page's `og:image` / `twitter:image`
(with width/height/alt) and surface it as a hero image at the top of
the README. The chop-seal mark on og-01 / og-04 is the actual
`Mora/Assets.xcassets/AppIcon.appiconset/AppIcon.png`, not arbitrary
kanji.
Ship a `tools/og/render-og.sh` headless-Chrome renderer. It works
around a headless quirk where Chrome reserves ~90px of phantom browser
chrome at the bottom of every screenshot — render at 1920×1170 and
crop the top 1920×1080 via Pillow so the rendered PNGs end exactly at
their natural board background instead of leaking the body bg.
Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a curated set of 1920×1080 OpenGraph/social cover images to the docs site, provides a local gallery for viewing them at scale, and wires the primary cover image into the site’s OG/Twitter metadata and README.
Changes:
- Add
docs/og.htmlgallery page that renders seven 16:9 cover compositions using the site’s design system. - Add
tools/og/render-og.shto headlessly render/crop the gallery boards intodocs/img/og/*.png. - Update
docs/index.htmlOG/Twitter meta tags and add a hero image toREADME.md.
Reviewed changes
Copilot reviewed 4 out of 12 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| tools/og/render-og.sh | New headless Chrome renderer + crop step for generating 1920×1080 OG PNG assets. |
| docs/og.html | New gallery page containing the seven OG cover compositions and “native” anchors for rendering. |
| docs/index.html | Switch og:image/twitter:image to img/og/og-01.png and add width/height/alt metadata. |
| README.md | Surface og-01.png as a top-of-README hero image linking to the docs landing page. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <header class="page-header"> | ||
| <h1>Mora · 16:9 Cover Art <em>Gallery</em></h1> | ||
| <p> | ||
| Six 1920×1080 thumbnails for OG image / YouTube. Click <code>Open native</code> on any board to view it full-size, then export with Chrome DevTools → <code>Capture node screenshot</code>, or run the headless render script: <code>tools/og/render-og.sh</code>. |
There was a problem hiding this comment.
The header copy says “Six 1920×1080 thumbnails…”, but this page now includes seven boards (Patterns 01–07, including the OpenDyslexic variant). Update the count so the page description matches what’s actually rendered.
| Six 1920×1080 thumbnails for OG image / YouTube. Click <code>Open native</code> on any board to view it full-size, then export with Chrome DevTools → <code>Capture node screenshot</code>, or run the headless render script: <code>tools/og/render-og.sh</code>. | |
| Seven 1920×1080 thumbnails for OG image / YouTube. Click <code>Open native</code> on any board to view it full-size, then export with Chrome DevTools → <code>Capture node screenshot</code>, or run the headless render script: <code>tools/og/render-og.sh</code>. |
| # :has(.thumb:target) to swap into a fullscreen, native-size view of just | ||
| # that board, which lets headless Chrome capture a pixel-perfect 1920×1080 | ||
| # screenshot per pattern. |
There was a problem hiding this comment.
The script header comment says the gallery uses :has(.thumb:target) to enter the native/fullscreen view, but docs/og.html uses .thumb:target selectors (no :has). Please update the comment to match the actual mechanism so future changes don’t get made based on incorrect CSS assumptions.
| # :has(.thumb:target) to swap into a fullscreen, native-size view of just | |
| # that board, which lets headless Chrome capture a pixel-perfect 1920×1080 | |
| # screenshot per pattern. | |
| # .thumb:target selectors to swap into a fullscreen, native-size view of | |
| # just that board, which lets headless Chrome capture a pixel-perfect | |
| # 1920×1080 screenshot per pattern. |
| # screenshot per pattern. | ||
| # | ||
| # Usage: tools/og/render-og.sh [pattern_number...] | ||
| # tools/og/render-og.sh # render 01..06 |
There was a problem hiding this comment.
The usage comment says the no-arg invocation renders 01..06, but the actual default also includes 01-dyslexic. Update the usage examples to reflect the real default output set (7 renders) to avoid confusion.
| # tools/og/render-og.sh # render 01..06 | |
| # tools/og/render-og.sh # render 01..06 and 01-dyslexic |
| CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" | ||
| REPO="$(cd "$(dirname "$0")/../.." && pwd)" | ||
| SRC="file://${REPO}/docs/og.html" | ||
| OUT="${REPO}/docs/img/og" | ||
|
|
||
| if [[ ! -x "$CHROME" ]]; then | ||
| echo "Could not find Chrome at: $CHROME" >&2 |
There was a problem hiding this comment.
CHROME is hard-coded to the macOS app bundle path, so the script fails on Linux/CI or on Macs where Chrome isn’t installed in /Applications (or where Chromium is used). Consider allowing CHROME to be overridden via an environment variable and/or falling back to command -v google-chrome / chromium / chrome for better portability.
| CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" | |
| REPO="$(cd "$(dirname "$0")/../.." && pwd)" | |
| SRC="file://${REPO}/docs/og.html" | |
| OUT="${REPO}/docs/img/og" | |
| if [[ ! -x "$CHROME" ]]; then | |
| echo "Could not find Chrome at: $CHROME" >&2 | |
| find_chrome() { | |
| local candidate | |
| candidate="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" | |
| if [[ -x "$candidate" ]]; then | |
| echo "$candidate" | |
| return 0 | |
| fi | |
| for candidate in google-chrome chromium chromium-browser chrome; do | |
| if candidate="$(command -v "$candidate" 2>/dev/null)"; then | |
| echo "$candidate" | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| CHROME="${CHROME:-$(find_chrome || true)}" | |
| REPO="$(cd "$(dirname "$0")/../.." && pwd)" | |
| SRC="file://${REPO}/docs/og.html" | |
| OUT="${REPO}/docs/img/og" | |
| if [[ -z "$CHROME" || ! -x "$CHROME" ]]; then | |
| echo "Could not find Chrome/Chromium. Set CHROME to a browser executable path or install google-chrome/chromium/chrome." >&2 |
| if [[ -x "$PY_BIN" ]]; then | ||
| "$PY_BIN" - "$CROP_H" "$WIN_W" "${patterns[@]/#/${OUT}/og-}" <<'PY' || true | ||
| import sys | ||
| try: | ||
| from PIL import Image | ||
| except ModuleNotFoundError: | ||
| print("Pillow not available — skipping crop. Install with: /tmp/og-venv/bin/pip install Pillow", file=sys.stderr) | ||
| sys.exit(0) | ||
| crop_h = int(sys.argv[1]); crop_w = int(sys.argv[2]) | ||
| for path in sys.argv[3:]: | ||
| if not path.endswith('.png'): | ||
| path = path + '.png' | ||
| try: | ||
| img = Image.open(path) | ||
| except FileNotFoundError: | ||
| continue | ||
| cropped = img.crop((0, 0, crop_w, crop_h)) | ||
| cropped.save(path) | ||
| PY | ||
| fi |
There was a problem hiding this comment.
The crop step suppresses all errors (... <<'PY' || true) and also exits 0 when Pillow is missing, which can silently leave behind 1920×1170 screenshots while still printing a “Rendered …” summary. If the cropped 1920×1080 output is required for the committed OG assets, prefer failing the script when crop can’t run (or at least emitting a clear non-success exit code unless an explicit "skip crop" flag is set).
| if [[ -x "$PY_BIN" ]]; then | |
| "$PY_BIN" - "$CROP_H" "$WIN_W" "${patterns[@]/#/${OUT}/og-}" <<'PY' || true | |
| import sys | |
| try: | |
| from PIL import Image | |
| except ModuleNotFoundError: | |
| print("Pillow not available — skipping crop. Install with: /tmp/og-venv/bin/pip install Pillow", file=sys.stderr) | |
| sys.exit(0) | |
| crop_h = int(sys.argv[1]); crop_w = int(sys.argv[2]) | |
| for path in sys.argv[3:]: | |
| if not path.endswith('.png'): | |
| path = path + '.png' | |
| try: | |
| img = Image.open(path) | |
| except FileNotFoundError: | |
| continue | |
| cropped = img.crop((0, 0, crop_w, crop_h)) | |
| cropped.save(path) | |
| PY | |
| fi | |
| if [[ ! -x "$PY_BIN" ]]; then | |
| echo "Python 3 is required to crop rendered screenshots to ${WIN_W}×${CROP_H}." >&2 | |
| exit 1 | |
| fi | |
| "$PY_BIN" - "$CROP_H" "$WIN_W" "${patterns[@]/#/${OUT}/og-}" <<'PY' | |
| import sys | |
| try: | |
| from PIL import Image | |
| except ModuleNotFoundError: | |
| print("Pillow is required to crop rendered screenshots. Install with: /tmp/og-venv/bin/pip install Pillow", file=sys.stderr) | |
| sys.exit(1) | |
| crop_h = int(sys.argv[1]); crop_w = int(sys.argv[2]) | |
| for path in sys.argv[3:]: | |
| if not path.endswith('.png'): | |
| path = path + '.png' | |
| try: | |
| with Image.open(path) as img: | |
| cropped = img.crop((0, 0, crop_w, crop_h)) | |
| cropped.save(path) | |
| except FileNotFoundError: | |
| print(f"Rendered screenshot not found: {path}", file=sys.stderr) | |
| sys.exit(1) | |
| PY |
Signed-off-by: Yutaka Kondo <yutaka.kondo@youtalk.jp>
Signed-off-by: Yutaka Kondo <yutaka.kondo@youtalk.jp>
Signed-off-by: Yutaka Kondo <yutaka.kondo@youtalk.jp>
Summary
docs/img/og/, rendered from a shared gallery page (docs/og.html) so they pull from the landing page's design system (Fraunces × Spectral × JetBrains Mono · paper / ink / vermilion / mora-orange / mora-teal · halftone, washi, phoneme bars, chop seal). Use as the site OG image, YouTube thumbnails, or social-card source material.og-01.pngintodocs/index.htmlasog:image/twitter:image(with width / height / alt) and surfaces it as a hero image at the top ofREADME.md. The chop-seal mark is the actualAppIcon.appiconset/AppIcon.png, not arbitrary kanji.tools/og/render-og.sh— a headless-Chrome renderer that works around a known headless quirk where Chrome reserves ~90px of phantom browser chrome at the bottom of every screenshot.The compositions
og-01.pngMora.wordmark, three yokai cards, AppIcon chopog-01-dyslexic.pngog-01but the wordmark is set in OpenDyslexic — the dyslexia-friendly typeface this product is built aroundog-02.pngb ← v,r ← l,s ← th)og-03.png5 days/100+ PRs/25.84 MB/0cloud callsog-04.pngog-05.pngog-06.pngWhy a render script (and not just hand-edited PNGs)
Headless Chrome's
--screenshotreserves ~90px of phantom browser chrome at the bottom of every capture, leaking the gallery body background into the rendered PNG. The script renders at 1920×1170 and crops the top 1920×1080 via Pillow so each board ends exactly at its own background (paper / ink / washi).Re-render any subset:
A note on the GitHub repo's social preview
GitHub's per-repo "social preview" image is a Settings UI feature, not a file in the repo — once this PR lands, upload
docs/img/og/og-01.png(orog-01-dyslexic.png) at https://github.com/youtalk/mora/settings under General → Social preview.Test plan
docs/og.htmlopens in a browser and renders the gallery (all seven boards visible at scale).tools/og/render-og.shre-generates all seven PNGs without errors and the bottom row of each PNG shows that board's intended background, not#1a1a1a.https://www.youtalk.jp/moraonce the change is deployed and confirmog:imageresolves toog-01.pngat 1920×1080.🤖 Generated with Claude Code