skills/typst-cards/SKILL.md
Generate PNG images for online communication — social media, carousels, infographics, posts — using Typst. Use this skill whenever the user wants to create slides, cards, visual posts or any digital graphic content, even if they don't explicitly mention Typst. The skill drives an interview about brand materials (logo, palette, fonts, DESIGN.md), proposes the formats best suited to the context (Instagram 1:1, Stories 9:16, LinkedIn 16:9, etc.) and produces ready-to-use PNGs.
npx skillsauth add ondata/skills typst-cardsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Turn a textual context and optional brand materials into professional PNG images for online communication, using Typst as the rendering engine. The skill manages the full flow: interview → theme → generation → review.
Before any other step, check that Typst is installed:
typst --version
If not available, install it like this (Linux/WSL x86_64):
curl -fsSL https://github.com/typst/typst/releases/latest/download/typst-x86_64-unknown-linux-musl.tar.xz -o /tmp/typst.tar.xz && tar -xf /tmp/typst.tar.xz -C /tmp/ && mkdir -p ~/.local/bin && mv /tmp/typst-x86_64-unknown-linux-musl/typst ~/.local/bin/
Note:
~/.local/binmust be in yourPATHfortypstto be found. Addexport PATH="$HOME/.local/bin:$PATH"to your shell profile if needed.
Before creating any file, ask these questions to the user in a single message:
Visual materials available?
DESIGN.md or brand guidelines to share?Desired output:
If the user has no brand materials, propose a palette consistent with the context and ask for a quick confirmation before proceeding.
| Name | Typical use | Pixels | width | height | PPI | |------|-------------|--------|-------|--------|-----| | Square 1:1 | Instagram post, carousels | 1080×1080 | 7.5in | 7.5in | 144 | | Portrait 4:5 | Instagram portrait | 1080×1350 | 7.5in | 9.375in | 144 | | Story 9:16 | Instagram/TikTok Stories, Reels | 1080×1920 | 7.5in | 13.33in | 144 | | Landscape 16:9 | Twitter/X, YouTube thumb, LinkedIn header | 1920×1080 | 13.33in | 7.5in | 144 | | LinkedIn/OG 1.91:1 | LinkedIn post, Open Graph meta | 1200×628 | 8.33in | 4.36in | 144 |
Math: pixels = inches × PPI → e.g. 7.5in × 144ppi = 1080px.
For carousels, always use the same format for every slide.
With DESIGN.md: read it and extract — and apply in theme.typ:
DISPLAY, SANS, MONO). For example: a serif display face for headlines, a sans-serif for body.Do not collapse everything into a single font — the display/body distinction is part of the brand.
With logo: note the path. You'll include it in Typst with:
#image("path/to/logo.png", height: 0.5in) // fixed height
#image("path/to/logo.svg", width: 2in) // fixed width
Without materials: pick a palette based on context:
#0d1117, accent blue #58a6ffAlways create this structure in the project directory:
<project>/carousel/
├── theme.typ # design tokens and helper functions
├── slides.typ # content (imports theme.typ)
└── output/ # generated PNGs
├── slide-1.png
├── slide-2.png
└── ...
Instead of writing from scratch, copy one of the reference templates from the skill folder and adapt it:
references/theme-starter.typ → palette, fonts, helpers (lbl, footer-block, codebox, divider)references/slides-1x1-starter.typ → 5 Instagram 1:1 slides (cover + 3 content + outro)references/slides-16x9-starter.typ → 3 LinkedIn 16:9 slides (cover + two-column + 3 stat)The templates are already tested and compile cleanly. They give you a proven structure; you change colors, fonts, content.
# example:
cp <SKILL_DIR>/references/theme-starter.typ <project>/carousel/theme.typ
cp <SKILL_DIR>/references/slides-1x1-starter.typ <project>/carousel/slides.typ
# then edit content in slides.typ and tokens in theme.typ
Adapt colors to the materials gathered. This is a starting point:
// — palette —
#let BG-DARK = rgb("#0d1117")
#let BG-LIGHT = rgb("#f6f8fa") // light background
#let BG-WHITE = rgb("#ffffff") // pure white
#let ACC = rgb("#58a6ff") // accent on dark background
#let ACC-L = rgb("#0969da") // accent on light background
#let FG-DARK = rgb("#e6edf3") // text on dark
#let FG-LIGHT = rgb("#1c2128") // text on light
#let MUTED-D = rgb("#8b949e") // secondary on dark
#let MUTED-L = rgb("#656d76") // secondary on light
#let CODE-BG = rgb("#161b22")
#let CODE-BR = rgb("#30363d")
// — fonts —
// Safe fonts on Linux/WSL: DejaVu Sans, DejaVu Sans Mono
// Check availability: fc-list | grep -i "Font Name"
#let SANS = ("DejaVu Sans", "Liberation Sans", "Arial")
#let MONO = ("DejaVu Sans Mono", "Liberation Mono", "Courier New")
// — helper: section label —
#let lbl(body, dark: false) = text(
size: 9pt, weight: "bold", tracking: 2pt,
fill: if dark { ACC } else { ACC-L },
)[#upper(body)]
// — helper: page footer (URL + native page counter) —
// Apply via #set page(footer: footer-block("github.com/me/repo"))
#let footer-block(url, dark: false) = context [
#line(length: 100%, stroke: 0.5pt + if dark { CODE-BR } else { rgb("#d0d7de") })
#v(0.06in)
#grid(columns: (1fr, auto),
text(font: MONO, size: 9pt, fill: if dark { MUTED-D } else { MUTED-L })[#url],
text(font: MONO, size: 9pt, fill: if dark { MUTED-D } else { MUTED-L })[
\# #counter(page).display() / #counter(page).final().first()
],
)
]
// — helper: code block —
#let codebox(body) = block(
fill: CODE-BG, stroke: 0.5pt + CODE-BR,
radius: 4pt, inset: (x: 14pt, y: 11pt), width: 100%,
)[
#set text(font: MONO, size: 10.5pt, fill: FG-DARK)
#body
]
#import "theme.typ": *
// Pick the format (only one active line). Keep `bottom` margin generous
// (~0.7in) so the footer line + 9pt text fit fully without clipping.
#set page(width: 7.5in, height: 7.5in, margin: (x: 0.55in, top: 0.5in, bottom: 0.7in), footer-descent: 18pt) // 1:1
// #set page(width: 7.5in, height: 9.375in, margin: (x: 0.55in, top: 0.5in, bottom: 0.7in), footer-descent: 18pt) // 4:5
// #set page(width: 7.5in, height: 13.33in, margin: (x: 0.55in, top: 0.6in, bottom: 0.75in), footer-descent: 20pt) // 9:16
// #set page(width: 13.33in, height: 7.5in, margin: (x: 0.85in, top: 0.55in, bottom: 0.72in), footer-descent: 20pt) // 16:9
// #set page(width: 8.33in, height: 4.36in, margin: (x: 0.55in, top: 0.4in, bottom: 0.6in), footer-descent: 16pt) // 1.91:1
#set text(font: SANS, size: 15pt, fill: FG-LIGHT)
// Footer URL — edit once for the deck. Counter is auto-numbered.
#let URL = "github.com/<org>/<repo>"
// — SLIDE 1 (dark) — set fill + matching dark footer together
#set page(fill: BG-DARK, footer: footer-block(URL, dark: true))
#v(1fr)
#lbl(dark: true)[tag · topic]
#v(0.15in)
#text(size: 44pt, weight: 900, fill: FG-DARK)[
Main title\
of the slide
]
#v(0.2in)
#text(size: 14pt, fill: MUTED-D)[Short subtitle or description]
#v(1fr)
// — SLIDE 2 (light) — switch fill → new page automatically
#set page(fill: BG-LIGHT, footer: footer-block(URL, dark: false))
#lbl[02 · section]
#v(0.2in)
#text(size: 31pt, weight: 900)[Section title]
#v(0.2in)
#text(size: 14pt, fill: MUTED-L)[Slide body text.]
// — SLIDE 3 (same background as slide 2 → explicit pagebreak) —
#pagebreak()
// ... content ...
Why the footer goes through #set page(footer: ...) and not a manual #ctr at the end of each slide: the page-level footer uses Typst's native page counter, so adding/removing slides requires no renumbering. It also lives in the bottom margin, so it cannot be pushed to the next page by dense content (see "fragile footer pattern" in pitfalls).
cd <project>/carousel
typst compile slides.typ "output/slide-{p}.png" --ppi 144
The {p} is replaced by the page number → one PNG per slide.
Text hierarchy: large title → subtitle → body. Max 3 levels. Don't crowd.
White space: generous margins (0.5–0.7in). Let the content breathe.
Colors: 2–3 max. A vivid accent on a neutral background always works.
Font sizes on a 1080px canvas (1pt Typst ≈ 2px output):
Carousels: counter at bottom right of every slide (1 / 6, 2 / 6, ...).
Story 9:16: center the content vertically, use larger fonts, avoid corners.
Footer/edge content never clips: any text near the page borders — footer, slide counter (ctr), source URL, page number — must be fully visible, no half-cut letters. If clipping occurs, increase the page bottom margin (#set page(margin: (x: ..., y: ...))) or reduce the footer font size. This is a must-check in Phase 4.
Grid with wide numbers/text: columns: (1fr, 1fr, 1fr) causes overflow if values are wide. Use a vertical layout or columns: (auto, 1fr) with a generous gutter.
Unavailable fonts: always specify fallbacks (("Chosen Font", "DejaVu Sans", "Arial")). Check with fc-list | grep -i "name". A warning is not an error: Typst uses the fallback.
#set page(fill: X) does not create a new page if X doesn't change: between slides with the same fill, use an explicit #pagebreak().
Fragile footer pattern — #v(1fr) + #ctr(n, total) as the last row of each slide: when content saturates the page, the 1fr space collapses to 0 and the counter slides onto the next page — you get a "ghost slide" with only the counter visible, no error. Prefer Typst's native page-level footer:
#set page(footer: footer-block(URL, dark: true))
// content of the slide — no v(1fr)+ctr at the end
The counter lives in the bottom margin and cannot be pushed by content. Overflow now manifests as a real extra page (immediately diagnosable) instead of a silent ghost slide.
v(1fr) works at page level: it splits the leftover space. If you place two of them, the space is split evenly between the two points.
Logo with transparent background: prefer SVG when possible. PNG with alpha works but requires that the slide background does not contrast badly with it.
Special characters in content mode: several characters open syntactic constructs in Typst content mode and cause "unclosed delimiter" errors when used literally. Escape with a backslash or rephrase:
| Char | What it opens in content | How to escape |
|------|---------------------------|----------------|
| $ | math mode | \$ |
| // | line comment | \/\/ or split with a space / / |
| _ (adjacent to a word) | emphasis (italic) | \_ |
| * (before a word) | strong (bold) | \* |
| @ | reference | \@ |
| < | label | use words ("less than") or $lt$ in math mode |
| ~ | non-breaking space (silent, rarely a problem) | \~ if you need it as literal output |
Typical contexts where you trip on this: terminal-style strings ($ command, ~$, exit_), paths like MIT // license, identifiers with _. Typst usually reports the error far from the actual cause, so when you see "unclosed delimiter" scan for these characters first.
Sub-case — strings passed as helper parameters are literal: when you call something like
#item("$ command", "...")or#text(...)[#param]with a string variable, the string is treated literally — backslash escapes are NOT resolved. So"\$ command"outputs\$ command(with the visible backslash), while"$ command"outputs$ commandcorrectly. The escape rule applies only inside content/markup, not inside string-typed parameter values.
leading is not a parameter of text(): it belongs to par(). To control line height of a text block:
// WRONG — error "unexpected argument: leading"
#text(size: 14pt, leading: 1.4em)[...]
// CORRECT — use par leading inside a block
#block[
#set par(leading: 0.7em)
#text(size: 14pt)[...]
]
align(center + horizon) with long text causes incorrect wordwrap: words can fuse. Prefer to handle vertical and horizontal alignment separately, or use block(width: 100%) to contain the text.
Editorial pattern (kicker + huge title + footer):
// magazine/newspaper style — battle-tested on 1:1
#text(size: 9pt, weight: "bold", tracking: 3pt, fill: ACC)[#upper("category · section")]
#v(0.08in)
#line(length: 100%, stroke: 1pt + ACC)
#v(0.25in)
#text(size: 14pt, fill: DIM)[Eyebrow]
#v(0.05in)
#text(size: 60pt, weight: 900)[Big title]
#v(0.05in)
#text(size: 18pt, weight: "bold", fill: ACC)[Accent subtitle]
#v(1fr)
#line(length: 100%, stroke: 0.5pt + rgb("#333333"))
#v(0.1in)
#grid(columns: (1fr, 1fr, 1fr),
text(size: 11pt)[Date],
align(center, text(size: 11pt)[Place]),
align(right, text(size: 11pt, fill: ACC)[CTA]),
)
After every compile:
Typst recompiles in <1s, so iteration is cheap; each round must still be driven by an explicit user decision.
development
Guides users step by step in drafting a formal complaint (segnalazione) to Italy's Digital Civic Defender (Difensore Civico per il Digitale, DCD) at AGID for violations of the CAD (Codice dell'Amministrazione Digitale) or other digitalization norms by public administrations. Use this skill whenever someone wants to: report an Italian PA to AGID; write to the Difensore Civico per il Digitale; complain about open data violations, non-machine-readable public data, inaccessible PA portals, missing or restrictive licenses on public data, captchas blocking automated access, unanswered data reuse requests (D.Lgs. 36/2006 art. 5), failure to publish mandatory High Value Datasets (HVD, Reg. (UE) 2023/138), or a prior DCD complaint that got no response. Trigger even if the user does not name the skill — any Italian digital-rights complaint targeting a PA is a candidate.
development
Create charts, choropleth maps, and locator maps via the Datawrapper API. Use this skill whenever the user wants to publish a visualization on Datawrapper, create an interactive chart or map from data, generate a PNG/embed from Datawrapper, or use the Datawrapper REST API. Triggers on: "create a map with datawrapper", "publish a chart on datawrapper", "choropleth map", "locator map datawrapper", "export PNG from datawrapper", and any request involving creating or configuring Datawrapper charts/maps programmatically. Also triggers for Italian variants: "mappa coropletica datawrapper", "crea grafico datawrapper", "mappa datawrapper".
development
Query OpenAlex API from the command line with curl and jq for publication discovery, filtering, sorting, pagination, and PDF availability checks. Use when searching scholarly works/authors/sources, building or debugging OpenAlex queries, extracting results, or downloading available PDFs using OPENALEX_API_KEY.
testing
Comprehensive open data quality validator for two audiences: data analysts who need to assess whether a dataset is ready to use, and public administrations who want to self-evaluate their published data. Automatically adapts based on input type: (A) local CSV file only — performs file-level structural and content checks; (B) CKAN/open data portal dataset — adds metadata completeness, resource accessibility, URL reachability, and DCAT-AP compliance (supports all national profiles: DCAT-AP 2.x baseline, IT, BE, NL, DE, FR, UK, ES, and others). Always use this skill when the user mentions: data quality, validate dataset, check CSV, open data compliance, metadata audit, CKAN dataset review, "is this data usable?", or whenever a CSV file or CKAN dataset ID/URL is provided for quality assessment. Produces severity-ranked reports (blocker / major / minor) with concrete fixes, quality score, and a plain-language summary for non-technical stakeholders.