Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Organization-level defaults for the **drumandbytes** GitHub org.
| [`profile/README.md`](profile/README.md) | Renders as the org landing page at [github.com/drumandbytes](https://github.com/drumandbytes). |
| [`profile/assets/`](profile/assets/) | Images used by the profile page (banner, etc.). |
| [`profile/scripts/update_profile.py`](profile/scripts/update_profile.py) | Regenerates the `BLOG-POSTS` block in the profile from the blog RSS feed. |
| [`profile/social/`](profile/social/) | Generator for the per-repo social-preview cards (uploaded to each repo's settings by hand — see its README). |
| [`.github/workflows/update-profile.yml`](.github/workflows/update-profile.yml) | Runs that script daily and commits the result (as the `dnb-robot` app). |
| [`.github/workflows/ci.yml`](.github/workflows/ci.yml) | actionlint + `ruff` + template validation on every PR; the gate for auto-merge. |
| [`.github/dependabot.yml`](.github/dependabot.yml) | Weekly grouped bumps for the actions used in `.github/workflows/`. |
Expand Down
67 changes: 67 additions & 0 deletions profile/social/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Social preview cards

Generates 1280×640 cards so `github.com/drumandbytes/*` links unfurl as a
branded set. A repo's social preview is a GitHub **setting** (no API) — build a
card here, then upload it at `<repo> → Settings → General → Social preview`.

## Files

| | |
|---|---|
| `template.html` | The card layout (dark ground, orange accent, name + tagline, optional device). |
| `devices.min.css` | [devices.css](https://github.com/picturepan2/devices.css) v0.2.0, MIT — pure-CSS device frames. Vendored. |
| `build.sh` | Renders one card: `./build.sh <slug> "<tagline>" [none\|phone\|tablet\|laptop] [screenshot.png]` |
| `build-all.sh` | Renders every row of `cards.tsv`. |
| `cards.tsv` | `slug ⇥ device ⇥ screenshot ⇥ tagline` per repo. |
| `screenshots/` | App screenshots for the device-framed cards. |
| `out/` | Generated PNGs. |

## Requirements

- **Chromium** for the screenshot step — `build.sh` points `BROWSER` at Brave;
override for another Chromium.
- **ImageMagick** (`magick`) only for the optional montage.

## Regenerating

```sh
./build-all.sh # all cards
./build.sh nordvpn "…" # just one
```

Edit taglines in `cards.tsv`. Keep them to ~1–2 lines.

## Capturing app screenshots (the device-framed cards)

Headless Chrome can't render the WebGL maps in `f1-walk` / `road-conditions-ee`.
[`shot-scraper`](https://shot-scraper.datasette.io/) (Playwright) can:

```sh
uvx shot-scraper install # one-time

# Teesilm — wait for the map + markers to load
uvx shot-scraper "https://roadconditions.drumandbytes.ee" \
-o screenshots/road-conditions-ee.png --width 400 --height 860 --retina --wait 12000

# f1-walk — dismiss the intro modal, then wait for the circuit map
uvx shot-scraper "https://f1walk.drumandbytes.dev/monaco" \
-o screenshots/f1-walk.png --width 400 --height 860 --retina --wait 3000 \
--javascript "[...document.querySelectorAll('button')].find(b=>/start the walk/i.test(b.textContent))?.click()" \
--wait 5000
```

For static pages a plain Brave `--headless --screenshot` is enough. The
committed screenshots are downscaled JPGs (`magick shot.png -quality 82 shot.jpg`)
to keep the repo light — `build.sh` reads `.jpg` or `.png`. `stacked` derives
its two frames from `screenshots/<slug>.*` (front) and `screenshots/<slug>-back.*`
(back).

`out/` is regenerated by `build-all.sh`; the copies here are just the current set.

## Design

Deliberately minimal: repo name (Arial-bold weight), one-line tagline, the site
accent `#FF6633` as a left rule, `drumandbytes.com` wordmark. Device-framed
cards add a tilted iPhone 14 on the right for the two PWAs; everything else is
type-only so the set stays coherent. `nordvpn` / `pve-metrics-exporter` are
type-only too — people share their GHCR package, not the repo.
18 changes: 18 additions & 0 deletions profile/social/build-all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
# Build every card listed in cards.tsv. Tab-separated columns:
# slug <TAB> device <TAB> screenshot-path <TAB> tagline
# device: none | phone | tablet | laptop (screenshot-path empty when none)
set -euo pipefail
cd "$(dirname "$0")"

TAB=$'\t'
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in ''|'#'*) continue ;; esac
slug=${line%%"$TAB"*}; rest=${line#*"$TAB"}
device=${rest%%"$TAB"*}; rest=${rest#*"$TAB"}
shot=${rest%%"$TAB"*}; tagline=${rest#*"$TAB"}
./build.sh "$slug" "$tagline" "${device:-none}" "$shot"
done < cards.tsv

echo
echo "Cards in ./out/ — upload each to <repo> Settings -> General -> Social preview."
96 changes: 96 additions & 0 deletions profile/social/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/bin/bash
# Social-preview card generator: HTML template -> headless screenshot.
#
# ./build.sh <slug> "<tagline>" [device] [screenshot.png]
#
# device:
# none (default) — type only
# phone | tablet | laptop — one framed screenshot on the right
# stacked — two phones, screenshots/<slug>-back.png behind screenshots/<slug>.png
#
# Needs a Chromium (BROWSER env) for --screenshot; `magick` only for the montage.
# Output: out/<slug>.png (1280x640).
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p out

BROWSER="${BROWSER:-/Applications/Brave Browser.app/Contents/MacOS/Brave Browser}"

slug="$1"; tag="$2"; device="${3:-none}"; shot="${4:-}"

case "$device" in
none) TEXT_W=1040; NAME_SIZE=62 ;;
*) TEXT_W=560; NAME_SIZE=54 ;;
esac

# base64 data-URI for a screenshot (png or jpg)
datauri() { # <path>
case "$1" in *.jpg|*.jpeg) m=image/jpeg ;; *) m=image/png ;; esac
printf 'data:%s;base64,%s' "$m" "$(base64 -i "$1" | tr -d '\n')"
}

# one devices.css phone/tablet/laptop element wrapping a screenshot data-URI
emit_device() { # <css-class> <data-uri> <inline-style>
cat <<HTML
<div class="device $1" style="$3">
<div class="device-frame">
<div class="device-screen" style="background-image:url($2)"></div>
</div>
<div class="device-stripe"></div><div class="device-header"></div>
<div class="device-sensors"></div><div class="device-btns"></div><div class="device-power"></div>
<div class="device-home"></div>
</div>
HTML
}

first_existing() { for f in "$@"; do [ -f "$f" ] && { echo "$f"; return; }; done; }

stage=""
case "$device" in
none) ;;
stacked)
back=$(first_existing "screenshots/${slug}-back.jpg" "screenshots/${slug}-back.png")
front=$(first_existing "screenshots/${slug}.jpg" "screenshots/${slug}.png")
[ -n "$back" ] && [ -n "$front" ] || { echo "stacked needs screenshots/${slug}-back.* and screenshots/${slug}.*" >&2; exit 1; }
bb=$(datauri "$back"); bf=$(datauri "$front")
stage="<div class=\"stage stacked\">
$(emit_device device-iphone-14 "$bb" "--s:.47; --rot:-11deg; --tx:-150px; --ty:-30px; z-index:1; filter:brightness(.72) drop-shadow(0 30px 50px rgba(0,0,0,.55));")
$(emit_device device-iphone-14 "$bf" "--s:.55; --rot:-2deg; --tx:-30px; --ty:26px; z-index:2; filter:drop-shadow(0 45px 70px rgba(0,0,0,.6));")
</div>"
;;
phone|tablet|laptop)
[ -n "$shot" ] && [ -f "$shot" ] || { echo "$device needs a screenshot path" >&2; exit 1; }
b64=$(datauri "$shot")
case "$device" in
phone) cls=device-iphone-14; st="--s:.64; --rot:-2deg; --ty:-14px;" ;;
tablet) cls=device-ipad-pro; st="--s:.50; --rot:-2deg; --ty:-14px;" ;;
laptop) cls=device-macbook-pro; st="--s:.62; --rot:0deg; --ty:0;" ;;
esac
st="$st filter:drop-shadow(0 45px 70px rgba(0,0,0,.6));"
stage="<div class=\"stage\">$(emit_device "$cls" "$b64" "$st")</div>"
;;
*) echo "unknown device: $device" >&2; exit 1 ;;
esac

esc() { python3 -c 'import html,sys;print(html.escape(sys.argv[1]))' "$1"; }

sed -e "s|__NAME__|$(esc "$slug")|g" \
-e "s|__TAG__|$(esc "$tag")|g" \
-e "s|__TEXT_W__|${TEXT_W}px|g" \
-e "s|__NAME_SIZE__|${NAME_SIZE}px|g" \
template.html > /tmp/_card.html
# $stage can be ~MB (embedded base64 images) — pass via file, not argv
printf '%s' "$stage" > /tmp/_stage.html
python3 - <<'PY'
import pathlib
card = pathlib.Path("/tmp/_card.html")
stage = pathlib.Path("/tmp/_stage.html").read_text()
card.write_text(card.read_text().replace("__STAGE__", stage))
PY
cp devices.min.css /tmp/devices.min.css

"$BROWSER" --headless=new --hide-scrollbars --force-device-scale-factor=1 \
--window-size=1280,640 --default-background-color=00000000 \
--virtual-time-budget=4000 --screenshot="out/$slug.png" "file:///tmp/_card.html" >/dev/null 2>&1

magick identify "out/$slug.png" 2>/dev/null || echo "wrote out/$slug.png"
9 changes: 9 additions & 0 deletions profile/social/cards.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# slug device screenshot tagline
eraser phone screenshots/eraser.jpg Free, open-source data-broker removal — automated GDPR / CCPA opt-out requests to 700+ brokers.
f1-walk stacked Fan-made PWA walking guides to real F1 street circuits — Monaco, Baku, Singapore, Las Vegas, Melbourne. GPS, corner history, offline.
road-conditions-ee phone screenshots/road-conditions-ee.jpg Teesilm — a faster, offline-capable PWA for Estonian road and weather conditions.
opentofu-updater-action none Keeps OpenTofu / Terraform providers, modules, Helm charts and images up to date via pull requests.
argocd-gitops-updater-action none Automated Helm chart and image version bumps for GitOps repos. ArgoCD and Kustomize aware.
nordvpn none Minimal, distroless NordVPN + Meshnet container image — no shell, no package manager, rebuilt weekly.
pve-metrics-exporter none Proxmox VE exporter for Prometheus and Glance — including the CPU / GPU / NVMe temperatures most exporters skip.
dotfiles none Personal macOS dotfiles managed with chezmoi — shell, Homebrew and app config, reproducible on a fresh machine.
1 change: 1 addition & 0 deletions profile/social/devices.min.css

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/out/dotfiles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/out/eraser.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/out/f1-walk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/out/nordvpn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/out/opentofu-updater-action.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/out/pve-metrics-exporter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/out/road-conditions-ee.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/screenshots/eraser.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/screenshots/f1-walk-back.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/screenshots/f1-walk.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added profile/social/screenshots/road-conditions-ee.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions profile/social/template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="devices.min.css">
<style>
:root {
--bg: #0D1117;
--accent: #FF6633;
--name: #FFFFFF;
--tag: #8B98A5;
--foot: #5B6B82;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1280px; height: 640px; overflow: hidden; }
body {
background: var(--bg);
font-family: ui-sans-serif, -apple-system, "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
position: relative;
}
.accent { position: absolute; left: 0; top: 0; bottom: 0; width: 14px; background: var(--accent); }

.text {
position: absolute; left: 100px; top: 50%; transform: translateY(-50%);
width: var(--text-w, 620px);
}
.name {
color: var(--name); font-weight: 800; letter-spacing: -0.02em;
font-size: var(--name-size, 60px); line-height: 1.05;
}
.tag {
color: var(--tag); font-weight: 400; font-size: 30px; line-height: 1.45;
margin-top: 28px;
}
.foot {
position: absolute; left: 100px; bottom: 60px;
color: var(--foot); font-size: 24px;
}

/* device(s) on the right of the card */
.stage {
position: absolute; right: 0; top: 0; bottom: 0; width: 560px;
display: flex; align-items: center; justify-content: center;
}
.stage .device {
transform:
translate(var(--tx, 0), var(--ty, 0))
scale(var(--s, 0.55))
rotate(var(--rot, 0deg));
}
.stage.stacked { display: block; }
.stage.stacked .device { position: absolute; top: 50%; left: 50%; margin: -420px 0 0 -215px; }
.device .device-screen {
background-position: center top;
background-size: cover;
background-repeat: no-repeat;
}
</style>
</head>
<body>
<div class="accent"></div>
<div class="text" style="--text-w: __TEXT_W__; --name-size: __NAME_SIZE__;">
<div class="name">__NAME__</div>
<div class="tag">__TAG__</div>
</div>
<div class="foot">drumandbytes.com</div>
__STAGE__
</body>
</html>