Skip to content
Draft
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
179 changes: 179 additions & 0 deletions .claude/skills/om-post/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
---
name: om-post
description: "Turn an Organic Maps release post into branded social creatives — 4:5, 1:1 and 9:16 PNGs plus a caption — localized into the Telegram channel languages and posted with the existing Telegram tooling. Use when asked to make a social post, Instagram carousel, release announcement graphics, or to attach creatives to a Telegram release announcement."
---

# Release post → social creatives

Input: a news post in `content/news/<date>/<n>/` — its `index.md` changelog and
the screenshots already sitting next to it.
Output: `social/<release>/export/<lang>/<format>/*.png` and `caption.txt`.

The creatives are what the Telegram channels get instead of raw screenshots,
one set per channel language.

## 1. Set up the folder

A news folder `content/news/2026-07-23/620` renders into `social/2026-07-23-620`
— `tools/telegram_post_all.py` derives that path itself, so keep the name.

```
social/2026-07-23-620/
post.toml # the English slide script — the only file to write by hand
post.ru.toml # generated by social_translate.py
html/ # generated, gitignored
export/ # final PNGs, gitignored — regenerate rather than commit
caption.txt
```

Screenshots stay where they are, in the news folder. A slide names one by
filename and `source` tells the renderer where to look, so no file is copied.

## 2. Write `post.toml`

```toml
release = "2026-07-23-620"
source = "content/news/2026-07-23/620" # where the screenshots live
lang = "en"
formats = ["4x5"] # add "1x1", "9x16" when the post is also for Instagram

[[slides]]
type = "cover" # cover | feature | list | cta
kicker = "Organic Maps"
title = "July Update"
subtitle = "One or two sentences that set up the carousel."

[[slides]]
type = "feature"
eyebrow = "iOS · Bookmarks"
title = "Pick any color"
body = "One sentence. What changed and why the reader cares."
media = "Colors for bookmarks and tracks.jpg"
device = "iphone" # iphone | android | phone | desktop | plain
theme = "blue" # green | blue | light | dark
bleed = 120 # optional, see below

[[slides]]
type = "list"
eyebrow = "Android"
title = "Smoother day to day"
theme = "light"
items = ["…", "…"] # 4-5 max, one line each ideally

[[slides]]
type = "cta"
title = "Get the July update"
body = "Free, offline, ad-free and open source."
url = "get.omaps.org"
badges = ["apple-appstore", "googleplay", "f-droid"]
```

Keys are all optional except `type` and `title`. Badge names are the filenames
in `static/images/badges/`; the logo comes from `static/logos/`. Nothing is
duplicated into the social folder — these are the assets the website serves.

An iOS screenshot is wrapped in an iPhone, an Android one in an Android phone.
Leave `device` out and the platform is read off the eyebrow — `iOS · Bookmarks`
picks the iPhone by itself, in every language.

**`bleed`** controls portrait mockups. By default a phone runs off the bottom
edge so it reads large instead of shrinking to a sliver. If the feature being
demonstrated sits low in the screenshot, set `bleed` to a smaller pixel number
(e.g. `120`) so it stays on canvas, or `false` to fit the whole phone.

See [references/layouts.md](references/layouts.md) for what each slide type
looks like and [references/copywriting.md](references/copywriting.md) for
turning changelog bullets into slide copy and a caption.

## 3. Localize

```
python3 tools/social_translate.py social/2026-07-23-620 --telegram
```

Writes `post.<lang>.toml` for the eleven non-English Telegram channels through
the same DeepL path as `translate_md.py` — project glossary, formality per
language, native quotes. Only prose is translated; `media`, `device`, `theme`,
`badges` and `url` are identifiers and stay verbatim.

The output is a **draft**, exactly like a translated news post. Read what it
warns about: a string much longer than its English source will be set smaller
by the renderer, and shortening the copy reads better than shrinking the type.

## 4. Build

```
python3 tools/social_build.py social/2026-07-23-620 --all-langs
```

Flags: `--only 4x5` and `--slide 3` to iterate fast on one image, `--langs ru`
for a single language.

The script renders each slide through headless Chrome at 2x, downsamples with
Pillow to the exact size, and fails loudly if a PNG is missing or came out as a
flat frame. Two Chrome quirks are already handled inside it and must not be
"simplified" away:

- top-level `data:` URLs hang Chrome forever, so HTML is always written to disk
and loaded over `file://`;
- `--screenshot` writes a valid PNG but never exits, so the process is polled
and killed. `timeout(1)` is not installed on this machine.

A throwaway `--user-data-dir` under `.cache/` keeps the user's own Chrome
profile untouched; it is deleted afterwards.

Longer translations do not overflow: an auto-fit pass in the page shrinks the
type scale until the slide fits, down to 62%. Below that the copy is too long
and needs rewriting.

## 5. Review before delivering

Always look at the rendered PNGs — build success does not mean it looks right.
`social_build.py` writes `export/<lang>/sheet-<format>.png`, a contact sheet of
every slide in that language. Read it, per language.

Check specifically:

- the feature being announced is actually visible in the screenshot, not cropped
off by the bleed;
- no title wraps to an ugly orphan word — shorten the copy rather than the type;
- 9:16 content clears the Stories UI (nothing in the top 250px / bottom 300px);
- long list slides did not overflow, and did not shrink so far they look off;
- right-to-left languages (ar, fa-IR) read correctly.

## 6. Post to Telegram

The existing poster picks the creatives up on its own — it looks for
`social/<release>/export/<lang>/4x5/` and sends that language's set to that
language's channel, falling back to English for a language that was not
rendered.

Rehearse on a throwaway channel first:

```
python3 tools/telegram_post_all.py content/news/2026-07-23/620 --dry-run
python3 tools/telegram_post_all.py content/news/2026-07-23/620 --only ru --to @my_test_channel
```

Then post for real — `--no-creatives` falls back to the raw screenshots:

```
python3 tools/telegram_post_all.py content/news/2026-07-23/620
```

**Never post to the real channels without being asked to.** Publishing is
irreversible; a rehearsal on a test channel is not.

## 7. Caption (Instagram)

Write `caption.txt`: a one-line hook, the highlights as short bullets, the
download link, then a hashtag block. Keep it under ~2 200 characters.
Details in [references/copywriting.md](references/copywriting.md). Telegram
takes the news post's own text and needs no caption.

## Design changes

All styling is in `assets/brand.css` — tokens, canvas formats, slide types and
device frames. `social_build.py` only emits structure and never inlines styles
beyond the per-slide `--bleed` override. Brand green is `#006C35`; type is the
system SF stack, since no webfont can be fetched offline.
Loading