
Postmark Card
Link preview images that arrive by air mail: one 1200x630 PNG per title, stamped by script, no image model.
Install
Install the "Postmark Card" skill into ~/.claude/skills/postmark-card. Download each file and save it under that folder: https://skilllift.pages.dev/packs/postmark-card/SKILL.md → SKILL.md https://skilllift.pages.dev/packs/postmark-card/references/style.md → references/style.md https://skilllift.pages.dev/packs/postmark-card/assets/card.html → assets/card.html https://skilllift.pages.dev/packs/postmark-card/assets/emblems.json → assets/emblems.json https://skilllift.pages.dev/packs/postmark-card/assets/keep-words.json → assets/keep-words.json https://skilllift.pages.dev/packs/postmark-card/scripts/render.mjs → scripts/render.mjs https://skilllift.pages.dev/packs/postmark-card/scripts/rules.mjs → scripts/rules.mjs https://skilllift.pages.dev/packs/postmark-card/scripts/check.mjs → scripts/check.mjs Then read SKILL.md, tell me what it does, and remind me to start a new session.
Unpack it into ~/.claude/skills
Same skill, different brief
The left is the preview. The right was made by a separate agent from a different brief, using only the installed skill and without seeing the preview.
One test run on a fictional brief. Letterforms can differ on systems without the same fonts.
Generates link preview images (Open Graph cards) at 1200x630 in an airmail correspondence style, one PNG per title, by script and with no image model. Takes one title or a CSV or JSON list, in English or Chinese. Use when someone needs share images, social cards or og:image pictures for a blog, newsletter, course or launch, especially many at once in a consistent look, or asks for this style by name.
What it must pass before delivery
node <skill folder>/scripts/check.mjs <folder>exits 0, and I read every WARN- I opened every PNG and its copy in
view-600/ - Every title is the user's wording, or I listed it as inferred
- No line break reads badly, even where the checker passed it
- The emblem suits each title; where unsure I used
envelope - Name is spelt as the user wrote it, or I listed the stand-in as an assumption
- I reported skipped and refused rows and every inferred date, label and colour
- Any made-up example has
noticeset tofictional - Nothing invented: no customers, numbers, issue numbers or credentials
Read the full SKILL.md
---
name: postmark-card
description: Generates link preview images (Open Graph cards) at 1200x630 in an airmail correspondence style, one PNG per title, by script and with no image model. Takes one title or a CSV or JSON list, in English or Chinese. Use when someone needs share images, social cards or og:image pictures for a blog, newsletter, course or launch, especially many at once in a consistent look, or asks for this style by name.
version: 1.0.0
---
# Postmark Card
The look lives in `assets/card.html` and never changes. You supply words; `scripts/render.mjs` draws the stamp, postmark, border and paper, fits the title by fixed size steps and writes a PNG. The user's own name and two colours are the only permitted changes. `scripts/check.mjs` compares each card with the engine byte for byte and inspects the pixels.
Exact tokens and geometry are in `references/style.md`. You only need them to answer questions.
## Inputs to ask for
Ask once, in one message, for anything the brief does not give. Then proceed.
| Field | Meaning | Limit | If the brief is thin |
|---|---|---|---|
| title (required) | The headline, one per card. Use the user's words exactly. `\|` sets a line break, `~` keeps two words together; neither prints | 100 units: Latin character 1, Chinese character 2. Five lines at most | Never write titles yourself unless asked. If you were asked, list them as inferred lines |
| name (required) | Who the card is from: the person or publication. Printed under FROM | 28 units, one line | Use "The Editor" and list it as an assumption (a Chinese card uses the stand-in given in SKILL.zh.md). The checker accepts these and prints a note. "Your Name" and similar are refused |
| ink | Title, postmark and half the border. Hex | At least 4.5:1 on the paper, 1.8:1 against accent | Leave out: default `#1b2a49`. Given two colours, the darker is ink. Given colour words such as "dark teal", choose a hex yourself and list it as inferred. Never guess a brand colour from a company name |
| accent | Stamp, small labels and half the border. Hex | At least 3:1 on the paper | Leave out: default `#c4311f`. Same rule for colour words |
| date | Date in the postmark. `YYYY-MM-DD` | A real date | Use the publication date if stated. Otherwise leave out: today is used. List it as inferred |
| label | Words round the postmark: a series or section. It decorates; do not rely on people reading it | 14 Latin or 8 Chinese characters | Leave out: the card prints its neutral word for "posted". Never invent an issue number |
| notice | A disclosure tag at the top of the card: `fictional` or `sample`. Prints fixed words, in the card's language, large enough to read at 600 wide | One of those two words | Leave out for real content. Use `fictional` for any made-up example, `sample` for a demonstration of real material. This is the way to label a card; never use the label for it |
| topic | What the publication or list is about, used to choose the emblem. Not printed. It outranks title words | 40 characters | Restate the user's own word for it, such as `cooking`. If unknown, leave out |
| emblem | The picture in the stamp. Default `auto` | One of the names below | Leave as `auto`. No clear match means the neutral `envelope` |
| emblem_why | Your reason for choosing an emblem the words do not support. Not printed | 60 characters | Leave out unless the user asked for that emblem |
| lang | `auto`, `en` or `zh`: language of the small printed words | | Leave as `auto`: each card follows its own title, which suits a bilingual list. For one language across a batch, pass `--lang`. Chinese wins when its characters, counted twice, reach the number of Latin letters |
In a CSV or JSON list, `title`, `date`, `label`, `notice`, `emblem`, `emblem_why` and `topic` can be set per row. Name and colours apply to the whole batch. For a bilingual publication with two names, render two batches.
### Emblems
Run `node <skill folder>/scripts/render.mjs --emblems` for this list with its uses:
`envelope` neutral, any topic · `compass` guides, strategy · `book` reading, teaching · `bulb` ideas · `key` security · `anchor` the sea · `leaf` nature, timber · `hammer` making, tools · `gear` engineering, machines · `code` programming, software · `pot` cooking, recipes · `cup` coffee, tea · `mountain` outdoors · `heart` health, care · `mic` podcasts, talks · `note` music · `bars` data, results · `coin` money, finance · `globe` world, languages · `plane` travel · `sailboat` sailing
How `auto` decides: a word in the topic wins. Title words only break a tie inside the topic, or decide when the topic matches nothing. Words match whole, so "fix" or "fixture" never suggest a tool. A tie or no match gives `envelope`.
**No fitting emblem: use `envelope`.** It is always allowed, and it is the only neutral one: `cup` means coffee or tea. Serious subjects (a death, a disaster, a war, layoffs) may only use `envelope`, whatever reason is given.
### Line breaks
Breaks are automatic, and known pairs such as Instant Pot and Chinese compounds stay together. When a break still reads badly, the remedy is in the title and needs no rewording:
- `Why Oak Moves|Across Seasons` breaks there. The marker does not print.
- `Bench~Dog` never breaks there. It prints as "Bench Dog"; between Chinese characters it prints no space.
- To print the character itself, write `\|` or `\~`.
## Steps
Below, `<skill folder>` is the folder holding this file, wherever it was installed. Both scripts work from any directory.
1. Install once: `npm install playwright`, then `npx playwright install chromium`. If Playwright sits elsewhere, set `NODE_PATH` to that `node_modules`.
2. Render:
```
node <skill folder>/scripts/render.mjs --title "Why Oak Moves" --name "Marrow & Grain" --topic woodworking --out cards/
node <skill folder>/scripts/render.mjs --list titles.csv --name "Marrow & Grain" --ink "#23352b" --accent "#b5541c" --out cards/
node <skill folder>/scripts/render.mjs --titles "First title; Second title; Third title" --name "Marrow & Grain" --out cards/
```
Pass a list in whichever form you were given it:
- **Pasted text:** `--titles`. Several lines give one title per line, with leading bullets or numbers removed. All on one line, it splits at semicolons. If a title contains a semicolon, use one line per title.
- **File:** `--list` with `.csv`, `.json` or `.txt`. A CSV without a header holds one title per row. With a header, `title` is required and the other columns above are optional. JSON is an array of strings or objects, or `{"titles":[...]}`. A `.txt` holds one title per line.
3. Read what it prints. A refused row names its reason and the other rows still render. Fix refused rows and run again.
4. Run `node <skill folder>/scripts/check.mjs cards/` and fix everything it reports. Exit 0 is required, so re-render without refused rows if the user has decided to drop them. Read each WARN too: it marks a line break that probably reads badly.
5. Look at every PNG at full size, and at the copy of the same name in `cards/view-600/`, which shows the size people see. Check the line breaks read naturally, the emblem suits the title, the stamp word and postmark label are readable, and the name is spelt correctly.
6. Deliver the PNG files, and list skipped rows, refused rows, inferred fields and assumptions.
Exit codes. `render.mjs`: 0 all rendered, 1 some rows refused, 2 refused outright (bad colours, bad options or an unreadable list). `check.mjs`: 0 pass, 1 problems, 2 bad usage.
Rendering takes under a second per card, including the browser start (three cards measured 0.7 s). Run one batch at a time. The script opens one browser and closes it.
## What happens to awkward input
| Input | Result |
|---|---|
| Very short title ("Hi", one Chinese character) | Renders on one line at the largest step. Nothing is added to fill the space |
| Long title | Steps down in size to 48px, five lines at most, and stays readable at 600 wide (24px there). If the best break at a size reads badly, one step smaller may be used |
| Over 100 units, or will not fit five lines | Row refused with TITLE_TOO_LONG or TITLE_DOES_NOT_FIT. Nothing is cut for you. Propose a shorter title and get it approved |
| One word wider than the title area at 48px | Row refused with TITLE_DOES_NOT_FIT. Hyphens and slashes are the only breaks inside a word |
| Mixed English and Chinese | Set together: each script uses its own typeface, a Latin phrase of up to three words stays on one line, and the small printed words follow `lang` |
| A semicolon list in a file | Row refused with ROW_MALFORMED, which says to use one title per row or `--titles` |
| CSV with commas and quotes | Standard rules: `"Oak, Ash, and ""Why"" Walnut"` prints as Oak, Ash, and "Why" Walnut |
| A comma without quotes, so a row has too many cells | Row refused with ROW_MALFORMED rather than losing half the title |
| Unclosed quote in a CSV | Whole file refused, with the line number |
| Empty row, or a blank title | Skipped and reported. Not a failure |
| Unsafe filename characters (`../../etc/passwd: a/b*?`) | The title prints as written. The file becomes `etc-passwd-a-b.png`, always inside the output folder |
| Numbered file names wanted | Rename the PNGs afterwards. The checker reads the record inside each file, so renamed and moved cards still pass. Re-saving or compressing a card removes the record and fails the check |
| Windows reserved name, or no letters or digits | Gets a `card-NNN` prefix |
| Identical titles | Second becomes `-2`, third `-3`, in row order |
| HTML in a title | Printed as plain text, never run |
| Unmatched bracket, or odd number of straight double quotes | Row refused with TITLE_BRACKET. Usually a CSV slip; if it is intended, reword it |
| Emoji | Row refused with TITLE_EMOJI. Letterpress type has no colour glyphs |
| Bad colour pair | Nothing renders. The message gives the nearest pair that passes. Ask the user, or use that pair and say so |
| Placeholder such as "Title" or "Your Name" | Row refused |
| No Chinese font on the machine | Row refused with FONT_MISSING. Install Noto Serif CJK SC |
Row numbers in messages are CSV line numbers, or positions in a JSON array counting from 1.
## When
- **The brief is thin:** restate the user's facts, never invent, and list every inferred line. Follow the table above field by field.
- **Nobody can answer:** proceed with the defaults above and list your assumptions at delivery.
- **The user wants a different length:** size is fixed at 1200x630 and the title is sized by the steps. A shorter title prints larger; you cannot set a size by hand. Rewrite only with their approval.
- **There is no real link:** the card holds no link, so none is needed. If you add an `og:image` tag, use the real address once the user has one. Never make one up.
- **The user wants another layout, font or a logo:** this pack does not do that. Say so.
## Deliverables
In the output folder: one `<title>.png` per card at exactly 1200x630, `cards.json` (what was made, skipped and refused) and `view-600/` (copies for looking at, not for publishing). Publish only the top-level PNGs. Each carries its title, name, date and layout record in a text chunk, which is harmless and readable by anyone.
## Before delivering
- [ ] `node <skill folder>/scripts/check.mjs <folder>` exits 0, and I read every WARN
- [ ] I opened every PNG and its copy in `view-600/`
- [ ] Every title is the user's wording, or I listed it as inferred
- [ ] No line break reads badly, even where the checker passed it
- [ ] The emblem suits each title; where unsure I used `envelope`
- [ ] Name is spelt as the user wrote it, or I listed the stand-in as an assumption
- [ ] I reported skipped and refused rows and every inferred date, label and colour
- [ ] Any made-up example has `notice` set to `fictional`
- [ ] Nothing invented: no customers, numbers, issue numbers or credentials
## When not to use it
- The user needs a photograph, a product shot or their logo on the card.
- A sombre or legal notice, where a playful airmail look would be wrong.
- Sizes other than 1200x630, or right-to-left scripts. Only English and Chinese are tested.
## Requirements
Node.js 22 or later and Playwright with Chromium. No image model and no network at render time. Type comes from the system: Rockwell or a similar slab serif for Latin, Songti SC or Noto Serif CJK SC for Chinese titles, and a Chinese sans for small Chinese words. The size steps are measured on the machine, so another machine's fallback font can change line breaks; the checker still applies.
Used in these workflows
- Launch a product step 4
- Grow a content account step 3
- Open an online shop step 6
- Promote a product step 4
- Build a personal site or portfolio step 5
- Refresh brand visuals step 5
- Run a campaign step 3



