diff --git a/.claude/skills/README.md b/.claude/skills/README.md new file mode 100644 index 0000000000..8ef224946a --- /dev/null +++ b/.claude/skills/README.md @@ -0,0 +1,67 @@ +# Skills + +Local skills for this repo. They help you write blog posts, create blog cover images, and write documentation in a consistent style. + +## How skills work + +- **With Claude Code:** every skill here loads automatically when you open the repo. Describe what you want ("write a blog post about X", "make a cover image for this post", "write a how-to for Y") and the matching skill activates. +- **Without Claude Code:** each skill is plain Markdown. Open its `SKILL.md` and follow the steps yourself. They are written to be read by people too. + +A skill is a folder with a `SKILL.md` (the instructions) and sometimes `references/`, `scripts/`, and `assets/`. + +## The skills + +| Skill | Use it to | Trigger example | +|-------|-----------|-----------------| +| [`content-write-blog`](content-write-blog/SKILL.md) | Scaffold a new Prisma blog post (frontmatter + section stubs) | "Draft a blog post about connection pooling" | +| [`content-create-hero-image`](content-create-hero-image/SKILL.md) | Generate a post's hero (SVG) and social/OG image (PNG) in the Eclipse house style | "Create a cover image for my Compute post" | +| [`docs-writer`](docs-writer/README.md) | Write or rewrite developer docs (how-to, concept, reference) | "Write a how-to for deploying to Prisma Compute" | + +--- + +## content-write-blog + +Produces a blog-post **skeleton** (frontmatter, section headings, short stubs), grounded in Prisma positioning. It writes content only; you own the git workflow (branch, commit, push, open the PR). + +**How to use:** + +1. Give it your pitch: the angle, audience, and key takeaway. (Or point it at a rough draft.) +2. Point it at your local checkout of this repo so it can read current blog conventions. +3. Give it your author slug. If you're a first-time author, it scaffolds an author profile too. +4. Confirm the proposed slug, filename, date, and frontmatter. +5. It writes the skeleton, then adds contextual links: the first mention of Prisma Postgres, Prisma Compute, and Prisma Next links to docs for SEO, and topic mentions cross-link to related posts (for example, a bloom-filter mention links to the bloom-index post). +6. It hands back a link inventory and a next-step reminder. You flesh out the prose and open a draft PR. + +**Note:** the skill reads `content-write-blog/assets/positioning.md`, Prisma's internal positioning doc. It is **not committed to this public repo** (it's gitignored). Place it locally before drafting, or the skill will ask for it. + +## content-create-hero-image + +Generates a `hero` (editable SVG) plus a pixel-exact `meta` (Open Graph PNG) for a blog post, in Prisma's Eclipse house style. It bundles the brand fonts, logos, design tokens, and a render pipeline, so the PNG matches the SVG exactly. + +**How to use:** + +1. Tell it the post the image is for (the topic, and the slug if the post exists). +2. It discovers your blog's current image conventions (directory, filenames, frontmatter fields) from recent posts. +3. It proposes a design direction (surface, accent, product, metaphor). Confirm or adjust. +4. It renders the SVG and PNG with `scripts/export-png.sh` and runs a built-in design-review pass. +5. It returns the saved `hero.svg` and `meta.png` paths (under `apps/blog/public//imgs/`) and the design rationale. + +For the render to use the real brand fonts, the bundled `assets/fonts/` is used automatically. See [`content-create-hero-image/README.md`](content-create-hero-image/README.md) for tooling details. + +## docs-writer + +Writes or rewrites developer-facing documentation a reader can follow without getting stuck. Covers how-to, concept, and reference pages. + +**How to use:** + +1. Tell it what to write and which page type ("write a how-to for X", "explain concept Y", "make this page clearer"). +2. It answers four scoping questions (reader, the one task, prerequisites, success signal), then drafts using the page shape and step rhythm in its `SKILL.md`. +3. It runs a final-pass checklist and cuts filler. + +See [`docs-writer/README.md`](docs-writer/README.md) for the one-minute writing style and [`docs-writer/references/how-to-use.md`](docs-writer/references/how-to-use.md) for a worked example of each page type. + +--- + +## Adding or changing a skill + +Edit the skill's `SKILL.md`. Keep the `description` in its frontmatter to "Use when..." trigger phrases only (that line is what routes a request to the skill). Put longer guidance in the body or in `references/`. diff --git a/.claude/skills/content-create-hero-image/README.md b/.claude/skills/content-create-hero-image/README.md new file mode 100644 index 0000000000..32892491bf --- /dev/null +++ b/.claude/skills/content-create-hero-image/README.md @@ -0,0 +1,108 @@ +# Create blog hero & meta images + +Produces an editable **SVG hero** and a **pixel-exact PNG meta** image for a Prisma blog post in +the Eclipse visual style, using the bundled brand tokens, logos, and fonts. The hero SVG is the +source of truth; the PNG is the export, and the meta image is always raster (Open Graph and social +cards do not render SVG). + +`SKILL.md` is the execution contract. This README is the usage and iteration guide. + +## Usage + +Invoke `content-create-hero-image` with the richest context you have: + +- a blog post path — `apps/blog/content/blog//index.mdx` +- a `prisma/web` PR URL (one hero/meta pair per changed post) +- a one-line title/pitch plus the product and target format + +**Interactive mode** — ask for guided/interactive mode (or invoke with only a thin prompt) and the +skill drives the brief with you in one round: the blog title or content, the one thing to +**highlight**, whether there's **copy** on the cover, what **must be present** (a logo, command, +metric, or metaphor), plus format and surface. It honors every required element and designs around +your highlight. + +```text +Use content-create-hero-image in interactive mode. The post is "Prisma Compute config file"; +highlight that one typed file targets every app; the prisma.compute.ts snippet must be present. +``` + +```text +Use content-create-hero-image for apps/blog/content/blog/prisma-compute-config-file/index.mdx. +Create a 1200x630 hero + meta and save hero.svg + meta.png under +assets/examples/prisma-compute-config-file/. +``` + +```text +Use content-create-hero-image for https://github.com/prisma/web/pull/7962. +Read the changed post and generate one hero/meta pair with the metaphor +"tokens append to a durable log before the UI tails them". +``` + +The skill discovers the blog's current conventions, reads the context, picks one concrete +metaphor, builds a layered hero SVG, embeds the fonts, exports a pixel-exact meta PNG, and saves +both to a durable path. + +## How to iterate + +Covers are a **render → critique → fix** loop. Don't judge the SVG source — judge the raster. +Critique with [`references/design-review.md`](references/design-review.md) — a structured pass +(first-impression → 10 categories graded A–F → AI-slop check → worst-first fixes), adapted from +the [gstack `design-review`](https://awesomeskill.ai/skill/gstack-gstack-design-review) skill. +Anything below a B is a finding; fix and re-render until every category clears. + +```bash +# 1. embed brand fonts into the SVG (do this BEFORE rendering) +python3 scripts/embed-fonts.py assets/examples//hero.svg + +# 2. render via headless Chrome — matches the SVG-in-browser exactly (real Mona Sans) +bash scripts/export-png.sh assets/examples//hero.svg /tmp/.png 1200 630 + +# 3. open /tmp/.png and look — margins, spacing, fonts, whitespace in code +# 4. edit hero.svg, then re-render. repeat until the SKILL.md checklist passes. + +# 5. regenerate the committed meta PNG so it matches the SVG +bash scripts/export-png.sh assets/examples//hero.svg assets/examples//meta.png 1200 630 +``` + +The PNG is rendered with **headless Chrome**, which honors the SVG's embedded `@font-face`, so the +`.png` is pixel-identical to the `.svg` opened in a browser. (rsvg/`rsvg-convert` ignores embedded +fonts and falls back to a generic sans — that was the old "PNG looks different from the SVG" bug.) +Set `SCALE=2` for a supersampled, extra-crisp render. Install `fonttools` + `brotli` for subset +embedding (~20–30 KB SVGs); without them the whole WOFF2 is embedded (larger, still self-contained). + +Editing a committed `hero.svg` is safe: the embedded `@font-face` block stays intact while you +change visual elements. Two recurring gotchas (see SKILL.md for the full list): + +- **Spaces vanish between ``s** — add `xml:space="preserve"` to multi-`tspan` lines (code + snippets especially), or `export default define...` runs together. +- **Diagram cards drift off the right edge** — keep ≥ 48px margin, give arrows real length, and end + every flow in a labeled node. + +## Output paths + +| Context | Path | +| ----------------------------- | --------------------------------------------------------------------------------- | +| Live blog post (`prisma/web`) | discovered `imgs/` dir, e.g. `apps/blog/public//imgs/hero.svg` + `meta.png` | +| Skill examples / samples | `assets/examples//hero.svg` + `meta.png` | + +## Samples + +One worked hero/meta pair per house content module — the quality bar for new work. + +| PR | Blog post | Module | Image | +| ----------------- | -------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------- | +| `prisma/web#7962` | `building-open-chat` | data/log panel | [meta.png](assets/examples/building-open-chat/meta.png) | +| `prisma/web#7957` | `create-prisma-deploy-prisma-compute` | terminal | [meta.png](assets/examples/create-prisma-deploy-prisma-compute/meta.png) | +| `prisma/web#7957` | `prisma-compute-config-file` | code card | [meta.png](assets/examples/prisma-compute-config-file/meta.png) | +| `prisma/web#7961` | `image-transformations-with-bun-on-prisma-compute` | pipeline/flow | [meta.png](assets/examples/image-transformations-with-bun-on-prisma-compute/meta.png) | +| — | `prisma-next-benchmark` | comparison card | [meta.png](assets/examples/prisma-next-benchmark/meta.png) | + +## Quality bar + +Read `references/design-system.md` before designing. Mona Sans for display, Inter for body, Geist +Mono for code/data. No stock imagery, generic AI visuals, logo wallpaper, random gradients, +off-system colors, or on-canvas taglines. One cover, one idea. + +Open items the team is still validating: the Eclipse cover direction on new product posts; Compute +and Prisma Next have no dedicated color tokens yet (they inherit platform teal); and the skill +depends on extracted Figma resources rather than a live source in every environment. diff --git a/.claude/skills/content-create-hero-image/SKILL.md b/.claude/skills/content-create-hero-image/SKILL.md new file mode 100644 index 0000000000..ce0a0e37b6 --- /dev/null +++ b/.claude/skills/content-create-hero-image/SKILL.md @@ -0,0 +1,348 @@ +--- +name: content-create-hero-image +description: Use when the operator wants a hero or meta image for a Prisma blog post; asks to create or generate a blog hero, cover, social card, Open Graph, or YouTube image; mentions cover art, a blog thumbnail, cover.svg/hero.svg/meta.png; references content-create-hero-image; or wants to interactively design cover imagery in Prisma's Eclipse house style. Produces an editable SVG hero plus a pixel-exact PNG meta image, and includes an interactive mode and a built-in design-review pass. +metadata: + author: Prisma + version: "2026.6.23" +--- + +# Create blog hero & meta images + +Produce the **hero** and **meta** (Open Graph) images for a Prisma blog post in the Eclipse +house style, styled from the bundled reference assets and from the most recent posts in the +operator's checkout, then wire them into the post's frontmatter. + +The hero is a hand-authored, layered **SVG** — the editable source of truth — rendered to a +**pixel-exact PNG**. The meta image is **raster (PNG)** because social platforms do not render +SVG. A single design usually serves both. Output is saved into the repo, not just returned in +chat. + +Companion to `content-write-blog` (which scaffolds the post itself). This skill makes the +imagery; it can be invoked standalone or as the cover step of that workflow. + +## Read first + +Use [`README.md`](README.md) when the operator asks how to use this skill or wants sample +prompts. Use this `SKILL.md` as the execution contract when actually producing assets. Read +[`references/design-system.md`](references/design-system.md) **before designing** and +[`references/design-review.md`](references/design-review.md) **before the design-review pass**. + +## References (load on demand) + +- [`references/design-system.md`](references/design-system.md) — colors, fonts, layout recipe, the **content-module catalog**, formats, sources. **Read before designing.** +- [`references/design-review.md`](references/design-review.md) — structured critique (A–F grades, AI-slop list, fix loop), adapted from the gstack `design-review` skill. **Read before the review pass.** +- [`assets/tokens.json`](assets/tokens.json) — machine-readable tokens; the source of truth for hex/font values. +- [`references/figma-source.md`](references/figma-source.md) — what `SOCIALS.fig` contains and how it was extracted. +- [`references/figma-mcp.md`](references/figma-mcp.md) — **optional**: pull live specs/assets from the Prisma Figma workspace when a Figma MCP is connected. +- [`assets/logos/`](assets/logos/) — official Prisma logo/symbol, Prisma Next mark + lockup, Postgres/Compute icons (`README.md`). +- [`assets/fonts/`](assets/fonts/) — bundled brand fonts (Mona Sans, Inter, Geist Mono); used by the scripts. +- [`assets/templates/cover.svg`](assets/templates/cover.svg) — parameterized starting template. +- [`assets/examples/`](assets/examples/) — one worked hero/meta pair per house content module; the quality bar for new work. +- [`assets/hero1.svg`](assets/hero1.svg)–[`hero4.svg`](assets/hero4.svg) — abstract reference heroes in the house style; study for palette, type, and motif alongside the worked examples. +- [`scripts/embed-fonts.py`](scripts/embed-fonts.py) — inline the brand fonts into the SVG as base64 `@font-face`. **Run first.** +- [`scripts/export-png.sh`](scripts/export-png.sh) — render the font-embedded SVG to PNG via headless Chrome so the PNG matches the SVG in a browser exactly. **Run second.** + +## Output contract + +Two images: a `hero` shown on the post itself, and a `meta` image for Open Graph and social +cards. A single design may serve both files. + +1. **Format.** + - **hero: SVG by default** — the prisma.io/blog standard, and it suits the typographic, + geometric heroes the blog favours. The SVG is the editable source of truth; always export + its PNG too. Fall back to a raster (PNG) hero only when the hero is genuinely photographic, + or when the operator asks for raster — and say so when you do. + - **meta: PNG (raster) always.** Open Graph and social cards do not render SVG. +2. **Dimensions.** `1200×630 px` (standard Open Graph size, ~1.9:1) for **both** hero and meta + by default. + - **SVG:** `viewBox="0 0 1200 630"` with `width`/`height` set to `1200`/`630`. + - **Raster:** `1200×630` at 1x (renders crisply on social cards, ~200–500 KB). A 2x export + (`2400×1260`) is acceptable for retina crispness only if it stays within budget; otherwise + drop back to 1x. Always preserve the 1.9:1 ratio. + - Other canvases (in-post hero `844×474`, YouTube `1280×720`, custom) are produced on request + — change `width`/`height`/`viewBox` and scale font sizes proportionally (≈ ×0.70 for the + 844-wide hero). +3. **Size budget.** Keep the meta PNG **under 1 MB** (1x normally lands ~200–500 KB; re-export + at 1x if a denser export overshoots). Keep the SVG lean: **subset, embedded fonts** (see step + 6) land each hero around ~20–30 KB. Prefer vector paths over embedded raster; flag any SVG + over ~1 MB. +4. **Naming.** Base names `hero` and `meta`, extension following the format: `hero.svg` (or + `hero.png` when raster) and `meta.png`. **No content hashes, no dimensions** in filenames. + For N explored directions, suffix the base name (`hero-a.svg`/`meta-a.png`, …). +5. **One design, both files.** When one design serves hero and meta, render the meta PNG from the + hero SVG so they are pixel-identical. +6. **Destination.** The image directory the recent posts use for the given `{slug}` (see + _Discover blog conventions_). Fall back per step 7 of the workflow only when discovery yields + nothing. + +## Pre-conditions — halt if unmet + +1. A blog post slug is known or can be derived. If the operator has not given one, ask for the + slug or the post title and derive the kebab-case slug. +2. The reference material is present: the bundled `assets/` (examples, logos, fonts, tokens) — or + the recent posts found during discovery. If nothing is available, ask the operator to supply + reference material before proceeding. + +## Discover blog conventions + +Never hardcode blog structure: the repo evolves. Before designing, learn the current conventions +from the operator's checkout. + +1. Find the last few published posts (most recent by date in filename or frontmatter). +2. From those posts, extract: + 1. **Image directory and filename pattern** for both hero and meta (e.g. + `public/{slug}/imgs/hero.svg`, `meta.png`, or whatever the recent posts use). + 2. **Frontmatter fields** that point at the hero and the meta image, and the exact value shape + each expects (public-relative path, relative path, etc.). + 3. **Prevailing hero format** (SVG vs raster) so the default matches what the blog ships. +3. Record the resolved directory, filenames, frontmatter fields, and value shapes; use them for + the rest of the workflow. + +## Inputs + +Blog context drives the design. Useful inputs: title, subtitle/excerpt, the Prisma product in +focus, target audience, the core technical concept, desired mood, required dimensions, and the +output format. A path to the post's `index.mdx` is the richest input — read its frontmatter and +lead. A GitHub PR URL is also valid input: read the PR title/body and any changed blog +`index.mdx` files before designing. + +## Workflow + +### 0. (Optional) Refresh from the live Figma workspace + +If a **Figma MCP is connected**, follow [`references/figma-mcp.md`](references/figma-mcp.md) to +reconcile `tokens.json` against the live Eclipse variables, sight the current cover frames for +layout/dimensions, and re-export any stale logos. Read-only — never edit the shared file. If no +Figma MCP is connected, skip this; the committed assets are the default. + +### 1. Read the blog context + +Extract: main product, core concept, audience, and intended emotional register (launch / +educational / conceptual / editorial / technical). Given an `index.mdx`, read the frontmatter +(`title`, `metaDescription`, `slug`) and the lead paragraph. Given a `prisma/web` PR URL, fetch +the PR metadata and changed blog files, then design one hero/meta pair per changed post unless the +operator asks for a single combined cover. + +### 2. Gather inputs — interactive or inferred + +Two ways to reach a design brief. Pick based on how the skill was invoked: + +- **Inferred (default).** Context is rich (an `index.mdx`, a PR, a clear pitch). Infer the brief + and ask only what you genuinely cannot — offer a recommended default with each, and prefer a + structured-choice question when the environment supports one. +- **Interactive.** The operator asks for interactive/guided mode, or gives only a thin prompt + ("make me a cover"). Drive the brief through the question set below in **one** structured-choice + round (don't interrogate one question at a time). Skip any question already answered by context, + and carry the recommended default into each. + +**Interactive question set.** Always read the post first, then ask a single structured-choice +round. **Make every question context-aware** — phrase it around this specific post and carry the +recommended default (derived from the content) into each option, so the author can accept the +whole set in one pass. Title each round with the post, e.g. _"Blog hero — 'Price the Work, Not +the Workflow'"_. + +| # | Question | Context to surface | Default | +| - | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| 1 | **Canvas size** | which format(s) this is for | Social/OG 1200×630 (offer in-post hero 844×474, YouTube 1280×720, custom) | +| 2 | **Text on the cover?** | "Most Prisma blog images carry **no copy** — the graphic + `Prisma` wordmark carry it. But this post has a strong short thesis that could work as a headline." | No copy, unless the thesis is short and punchy | +| 3 | **Custom copy** (if text) | quote the post's own thesis as the suggestion | the post's thesis verbatim; blank = no text | +| 4 | **What should the graphic symbolise?** | restate the post's core idea in concrete terms | the literal mechanism from the post | +| 5 | **Isometric skew on the graphic?** | "Default yes for card/table modules; flow/loop diagrams usually read best **flat**." | yes for cards/tables, flat for flows | +| 6 | **Background family** | name the product's surface | teal (Postgres/Compute/Next/platform); indigo only for ORM | +| 7 | **Mood** | launch / educational / conceptual / editorial / technical | educational, dark | +| 8 | **Product logo** | which mark, if any | the post's product mark when it clarifies; else just the `Prisma` wordmark | +| 9 | **How many directions to explore?** | 1–4 distinct concepts | 1 (offer up to 4) | + +Turn the answers into the brief(s) in step 3. **No-copy is a first-class, common choice** — when +chosen, the graphic + `Prisma` wordmark carry the cover. If **N directions** are requested, +produce N separate `hero`/`meta` pairs (`hero-a`, `hero-b`, …) and present them together. Honor +every "must be present" element and the chosen logo/skew/background; creative direction still +obeys the anti-patterns and the design-review bar — creative ≠ slop. + +### 3. Choose a visual direction + +Decide a concept that's literal-but-elegant, never generic. Pick: surface (dark default / light +editorial), product accent, eyebrow label, the **content module** that fits the content (see +`design-system.md` → _Content modules_: pipeline/flow, data/log panel, terminal, code card, +comparison card), and whether a product lockup belongs in the composition. Anchor every choice in +`design-system.md`. One accent, one idea, strong hierarchy, generous space. + +Before drawing, write a compact design brief for yourself: + +- **Message:** the one sentence the cover must communicate. +- **Metaphor / module:** the concrete visual structure, preferably from the post itself (log, + stream, config file, deploy target, image pipeline, chart, …). +- **Product signal:** the product logo/icon to use, or "Prisma only" if no product owns it. +- **Output path:** the discovered blog asset path, the example path, or the fallback path. + +### 4. Select assets + +From `assets/logos/`: use an official logo or product mark only when it clarifies the idea. +Default to **no footer chrome**. If a brand sign-off is wanted, place the `Prisma` wordmark in +the **bottom-left as plain text with no logo mark beside it**, or use a single official product +lockup as the subject — never pair an icon with extra "Prisma" text, and **never** add +`prisma.io/blog` to the canvas. Drop the wordmark entirely rather than crowd the frame. Pull exact +hex/fonts from `tokens.json`; do not introduce off-system colors, fonts, or stock imagery. + +### 5. Generate the SVG + +Copy `assets/templates/cover.svg` and fill the `{{TOKENS}}`, or hand-compose when the graphic is +the message (e.g. a chart) — keep the layer groups (`background`, `badge`, `headline`, `brand`, +`chart`) so it stays editable. Hand-wrap the title into 1–3 lines (drop title size to 56px if > 30 +chars). Set the eyebrow/badge pill width to roughly `(label length × 14) + 48`. Include the +``/`<desc>` metadata. + +**Fonts (avoid the #1 off-brand bug):** the brand faces are **Mona Sans** (headings/display, 800), +**Inter** (body, 400), and **Geist Mono** (data/code, 500), bundled in `assets/fonts/`. Do **not** +use Barlow — it is the legacy docs face and reads off-brand. A wrong family/weight falls back to a +generic sans. For emphasis, change colour or size, **not** weight. When the graphic _is_ the +message, let it own the canvas — center it, drop competing chrome, use the standalone product logo +as the brand mark, and label data with real numbers. + +Keep text short. Don't repeat the full blog title when a sharper phrase reads better — use the post +title in `<title>` metadata and a shorter headline on canvas. Supporting lines must be factual +labels, not taglines. Avoid fluffy copy, ellipses, and stacked sentence fragments. + +**Diagram cards (the right-side metaphor):** keep the card off the canvas edge — **≥ 48px margin** +on the right (1200-wide canvas → card right edge ≤ ~1128). Position absolutely, not via a group +`transform` that pushes past the edge. Inside: give arrows real length (~40–56px shaft + +arrowhead), pad content evenly from the card walls, and make every node **labeled and connected** +— a flow should end in a recipient (`client`, `webp` file, deploy target), not a dangling arrow. +Tie any header number to the data it labels, and highlight the "live"/current element. + +**SVG text rendering gotchas (librsvg/rsvg-convert):** + +- **Whitespace between `<tspan>`s is stripped.** With the default `xml:space`, librsvg trims + whitespace on each `tspan`, so `export default` + `<tspan> defineComputeConfig` renders as + `export defaultdefineComputeConfig`. For any multi-`tspan` line that needs internal spaces (code + snippets especially), put **`xml:space="preserve"`** on the parent `<text>`/`<g>`, and keep the + text content on a single source line so `preserve` doesn't pull in indentation. +- **Long words don't wrap.** SVG `<text>` has no auto-wrap — hand-split into `<tspan>` lines with + explicit `x`/`dy`. +- **Centering: avoid `text-anchor="middle"` + `dx`.** For a bold-name + quiet-label pair (e.g. + `api` / `Hono`), stack two single-run centered lines on the same `x`. Always **measure** + centering against the rendered PNG — never eyeball the source. + +### 6. Embed fonts FIRST, then export the PNG + +Order matters — the PNG must be rendered from the **font-embedded** SVG so the raster and the SVG +are pixel-identical: + +```bash +python3 scripts/embed-fonts.py <hero.svg> # 1. inline brand fonts +bash scripts/export-png.sh <hero.svg> <meta.png> 1200 630 # 2. render PNG (Chrome) +``` + +`embed-fonts.py` inlines Mona Sans/Inter/Geist Mono as base64 `@font-face` — subsetting to the +glyphs the SVG uses (with `fonttools` + `brotli`, ~20–30 KB) or embedding the whole WOFF2 when +those are unavailable (larger, still self-contained). `export-png.sh` renders with **headless +Chrome/Chromium**, which honors `@font-face`, so the PNG looks exactly like the SVG in a +browser/Figma. (librsvg/`rsvg-convert` ignores `@font-face` and falls back to a generic sans — the +old "PNG looks different from the SVG" bug. rsvg/magick are kept only as warned fallbacks.) Chrome +renders at 1x by default; set `SCALE=2` for a supersampled, extra-crisp render. If you edit a +committed SVG (fonts already embedded), just re-run `export-png.sh`. + +### 7. Save into the repo + +Save to a **durable** location, never a temp dir. Resolve in this order: + +1. **Discovered blog convention (preferred for live blog assets).** Use the directory, + filenames, and frontmatter fields resolved in _Discover blog conventions_ — e.g. + `apps/blog/public/{slug}/imgs/hero.svg` + `hero.png`, social/OG → `meta.png`. Fall back to + `apps/blog/public/{slug}/imgs/` only when discovery yields nothing. +2. **Skill examples.** When the operator asks for examples, samples, or quality references for + this skill, save under `assets/examples/<slug>/hero.svg` + `meta.png`, keeping `<slug>` aligned + with the blog post slug. +3. **Fallback** (no web checkout / generating ahead of the post): `blog-covers/<slug>/` at the + repo root with clear per-format names; tell the operator to move them into the post's `imgs/` + folder when the post lands. + +Always announce the absolute path before writing outside the current repo. + +### 8. Wire into the post frontmatter + +Set the resolved hero and meta frontmatter fields (from discovery) to the resolved value shapes. +If a field already exists, confirm it points at the new file. Report the exact frontmatter lines. + +### 9. Design review (render → critique → fix loop) + +Cover design is a render → critique → fix loop, not a one-shot. Run the structured review in +[`references/design-review.md`](references/design-review.md) — critique the **rendered PNG**, never +the SVG source: + +1. **Open the PNG and react** — one honest first-impression sentence (premium, or AI filler?). + Most defects — cramped margins, stripped whitespace in code, stubby arrows, a node touching the + edge — are invisible in the markup and obvious in the raster. +2. **Grade the 10 categories A–F** (hierarchy, type, colour, spacing, composition, metaphor, + render fidelity, thumbnail legibility, brand, AI-slop). Anything below B is a finding. +3. **Fix worst-first, minimally**: locate in the SVG → smallest fix → re-render → re-inspect → + classify `verified`/`best-effort`/`reverted`. Repeat until every category is ≥ B. +4. When refining a committed example, edit the `hero.svg` (the embedded `@font-face` survives edits + to visual elements), then **regenerate `meta.png`** so the committed raster matches. + +### 10. Return paths + rationale + +Report the saved SVG and PNG paths and 2–3 sentences on the design direction (surface, accent, +product, why the metaphor fits). Example: + +```text +hero: apps/blog/public/query-insights-ga/imgs/hero.svg +meta: apps/blog/public/query-insights-ga/imgs/meta.png +Direction: Dark Eclipse surface with the Postgres teal accent; Query Insights framed as a GA +launch via the pill eyebrow; restraint keeps it premium and thumbnail-legible. +``` + +## Validation — must pass before finalizing + +Visually inspect the rendered PNG, then verify: + +- [ ] Dimensions are `1200×630` (~1.9:1) for both images (or the requested format, ratio + preserved). SVG declares `viewBox="0 0 1200 630"` with matching `width`/`height`. +- [ ] The meta image is **PNG**; the hero is **SVG** (raster hero only on an explicit photographic + request). Each asset is self-contained — the SVG resolves with no external references. +- [ ] Colors and gradient match `tokens.json` exactly — no off-system values. +- [ ] Fonts render in real Mona Sans/Inter/Geist Mono (not a fallback sans, and **not Barlow**); + heed the export warning. The committed SVG has embedded `@font-face` (ran `embed-fonts.py`). +- [ ] One accent, one idea. Clear hierarchy; generous negative space. If a graphic is the message, + it is centered and uncluttered. +- [ ] Diagram cards sit ≥ 48px off the canvas edge; arrows are full-length; no node is jammed + against a wall; every flow ends in a labeled recipient. +- [ ] Code/snippet text renders its spaces correctly (multi-`tspan` lines carry + `xml:space="preserve"`). +- [ ] Title text is legible as a small thumbnail **and** at full 1200px social-preview size. +- [ ] Any logo/lockup is intentional, correctly colored, not stretched, and not paired with extra + label text. The `Prisma` wordmark, if used, sits bottom-left with no mark beside it. +- [ ] Any data/chart uses real numbers and the framing the prompt asked for. +- [ ] The canvas does not contain `prisma.io/blog`. +- [ ] **No** noisy background, random gradient, stock illustration, or unrelated decoration. +- [ ] Size budget: meta PNG **under 1 MB**; SVG lean (subset fonts, vector paths). +- [ ] SVG is layered/grouped and includes `<title>`/`<desc>`. +- [ ] Both files saved to a durable repo path (not temp), and the resolved frontmatter fields point + at them. + +## Anti-patterns + +- **Hardcoding blog conventions.** The repo evolves — always discover the image directory, + filenames, and frontmatter fields from recent posts; the documented standard is a fallback. +- **An SVG meta image.** Open Graph and social cards do not render SVG; the meta image is always + raster. +- **A raster hero by default.** SVG is the house standard for the hero; use PNG only when the hero + is genuinely photographic and the operator asked for it. +- **Generic AI look.** Glows everywhere, faux-3D blobs, busy gradients, literal robots. Prisma + covers are restrained and typographic. When in doubt, remove an element. +- **Off-system styling.** Inventing colors, or swapping the Mona Sans + Inter pairing (Barlow is + off-brand). +- **Decorative logos.** Marks are a footer sign-off or the subject — never wallpaper. +- **Centering text-only layouts.** Default covers are left-anchored with right-side breathing room. + Center only a single graphic-led composition. +- **Content hashes or dimensions in filenames.** Keep the base names `hero` and `meta`; the + extension follows the format and nothing else. +- **PNG-only or SVG-only.** SVG is the editable source of truth; the PNG is the export. Produce + both for the hero; the meta is always PNG. +- **Saving to temp.** Final assets live in a durable repo location. +- **Fabricating product palettes.** Postgres = teal, ORM = indigo. Compute/Next have no official + tokens yet — use the platform teal and say so; don't invent a brand color. +- **Tagline filler.** Avoid lines like `Resize. Encode. Cache.` If a secondary line is needed, + make it concrete and informational. +- **Copying a single reference.** Synthesise the house style from across the examples and recent + posts' images; do not clone one asset. diff --git a/.claude/skills/content-create-hero-image/assets/examples/building-open-chat/hero.svg b/.claude/skills/content-create-hero-image/assets/examples/building-open-chat/hero.svg new file mode 100644 index 0000000000..da558a026e --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/examples/building-open-chat/hero.svg @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Sample for prisma/web#7962: Open Chat / durable token streaming. --> +<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" fill="none" role="img" aria-labelledby="cover-title cover-desc"> + <title id="cover-title">How I Built a Chat App That Never Drops a Token + Open Chat cover showing tokens appended to a durable log before being tailed to the UI. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OPEN CHAT / DURABLE STREAMING + + Chat that never + drops a token + + Durable log backed streaming + + + + + DURABLE LOG + head · offset 412 + + + + 409 message.created + + + + 410 message.delta + + + + 411 message.delta + + + + 412 message.delta + + + + SSE tail + + + resumes at 412 + + + + + + client + + + + Prisma + + diff --git a/.claude/skills/content-create-hero-image/assets/examples/building-open-chat/meta.png b/.claude/skills/content-create-hero-image/assets/examples/building-open-chat/meta.png new file mode 100644 index 0000000000..37932877e4 Binary files /dev/null and b/.claude/skills/content-create-hero-image/assets/examples/building-open-chat/meta.png differ diff --git a/.claude/skills/content-create-hero-image/assets/examples/create-prisma-deploy-prisma-compute/hero.svg b/.claude/skills/content-create-hero-image/assets/examples/create-prisma-deploy-prisma-compute/hero.svg new file mode 100644 index 0000000000..d5785f0d4e --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/examples/create-prisma-deploy-prisma-compute/hero.svg @@ -0,0 +1,72 @@ + + + + Deploy Prisma Apps with create-prisma + A create-prisma flow scaffolds a deploy-ready app with typed defaults. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CREATE-PRISMA + + Scaffold a + deploy-ready app + + + + + + + + + $ bun create prisma@latest + + + + + Choose template + + + + Generate typed config + + + + Add Postgres + Skills + + + + Prisma + + diff --git a/.claude/skills/content-create-hero-image/assets/examples/create-prisma-deploy-prisma-compute/meta.png b/.claude/skills/content-create-hero-image/assets/examples/create-prisma-deploy-prisma-compute/meta.png new file mode 100644 index 0000000000..b66db0f58a Binary files /dev/null and b/.claude/skills/content-create-hero-image/assets/examples/create-prisma-deploy-prisma-compute/meta.png differ diff --git a/.claude/skills/content-create-hero-image/assets/examples/image-transformations-with-bun-on-prisma-compute/hero.svg b/.claude/skills/content-create-hero-image/assets/examples/image-transformations-with-bun-on-prisma-compute/hero.svg new file mode 100644 index 0000000000..9389aa8616 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/examples/image-transformations-with-bun-on-prisma-compute/hero.svg @@ -0,0 +1,75 @@ + + + + Image Transformations with Bun on Prisma Compute + A request-time pipeline turns a source image into a cached WebP output through Bun.Image. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + REQUEST-TIME IMAGE PIPELINE + + Image transforms + as app logic + + + + + + + + + + + source + Bun.Image + webp + + + Prisma + + diff --git a/.claude/skills/content-create-hero-image/assets/examples/image-transformations-with-bun-on-prisma-compute/meta.png b/.claude/skills/content-create-hero-image/assets/examples/image-transformations-with-bun-on-prisma-compute/meta.png new file mode 100644 index 0000000000..2a260f306b Binary files /dev/null and b/.claude/skills/content-create-hero-image/assets/examples/image-transformations-with-bun-on-prisma-compute/meta.png differ diff --git a/.claude/skills/content-create-hero-image/assets/examples/prisma-compute-config-file/hero.svg b/.claude/skills/content-create-hero-image/assets/examples/prisma-compute-config-file/hero.svg new file mode 100644 index 0000000000..e5430be503 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/examples/prisma-compute-config-file/hero.svg @@ -0,0 +1,78 @@ + + + + Configure Prisma Compute deploys in TypeScript + A typed prisma.compute.ts contract maps app targets from one clean config surface. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PRISMA COMPUTE CONFIG + + One typed file + for every app + target + + + + + + prisma.compute.ts + + export default defineComputeConfig({ + apps: { + api: app("apps/api", "hono"), + web: app("apps/web", "nextjs") + } + }) + + + + + + + + + + + api + Hono + + web + Next.js + + + Prisma + + diff --git a/.claude/skills/content-create-hero-image/assets/examples/prisma-compute-config-file/meta.png b/.claude/skills/content-create-hero-image/assets/examples/prisma-compute-config-file/meta.png new file mode 100644 index 0000000000..5a1134fec0 Binary files /dev/null and b/.claude/skills/content-create-hero-image/assets/examples/prisma-compute-config-file/meta.png differ diff --git a/.claude/skills/content-create-hero-image/assets/examples/prisma-next-benchmark/hero.svg b/.claude/skills/content-create-hero-image/assets/examples/prisma-next-benchmark/hero.svg new file mode 100644 index 0000000000..1a2e3fe3d5 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/examples/prisma-next-benchmark/hero.svg @@ -0,0 +1,58 @@ + + + + Prisma Next is ~87% as fast as raw Postgres + Throughput vs raw pg. pg (raw driver) 14,350 req/sec; Prisma Next 12,500 req/sec, about 87% of raw. + + + + + + + + + + + + + + + + + + + THROUGHPUT VS RAW PG + + Prisma Next is + ~87% as fast + as raw Postgres + + + + + + + + + pg (raw driver) + 14,350req/sec + + + + + + Prisma Next + 12,500req/sec + + + + + Prisma + diff --git a/.claude/skills/content-create-hero-image/assets/examples/prisma-next-benchmark/meta.png b/.claude/skills/content-create-hero-image/assets/examples/prisma-next-benchmark/meta.png new file mode 100644 index 0000000000..7d6d5bacf0 Binary files /dev/null and b/.claude/skills/content-create-hero-image/assets/examples/prisma-next-benchmark/meta.png differ diff --git a/.claude/skills/content-create-hero-image/assets/figma/board-meta.json b/.claude/skills/content-create-hero-image/assets/figma/board-meta.json new file mode 100644 index 0000000000..be6429571b --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/figma/board-meta.json @@ -0,0 +1 @@ +{"client_meta":{"background_color":{"r":0.9713040590286255,"g":0.9713040590286255,"b":0.9713040590286255,"a":1},"thumbnail_size":{"width":76,"height":400},"render_coordinates":{"x":-2894,"y":-8890,"width":8060,"height":42422}},"file_name":"SOCIALS","developer_related_links":[],"exported_at":"2026-05-13T13:17:31.007Z"} \ No newline at end of file diff --git a/.claude/skills/content-create-hero-image/assets/figma/board-thumbnail.png b/.claude/skills/content-create-hero-image/assets/figma/board-thumbnail.png new file mode 100644 index 0000000000..eac9d85c70 Binary files /dev/null and b/.claude/skills/content-create-hero-image/assets/figma/board-thumbnail.png differ diff --git a/.claude/skills/content-create-hero-image/assets/figma/examples/prisma-next-announcement.svg b/.claude/skills/content-create-hero-image/assets/figma/examples/prisma-next-announcement.svg new file mode 100644 index 0000000000..a19731a1a6 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/figma/examples/prisma-next-announcement.svg @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.claude/skills/content-create-hero-image/assets/fonts/GeistMono.woff2 b/.claude/skills/content-create-hero-image/assets/fonts/GeistMono.woff2 new file mode 100644 index 0000000000..4696edd453 Binary files /dev/null and b/.claude/skills/content-create-hero-image/assets/fonts/GeistMono.woff2 differ diff --git a/.claude/skills/content-create-hero-image/assets/fonts/Inter-Regular.woff2 b/.claude/skills/content-create-hero-image/assets/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000000..4a23fc64b9 Binary files /dev/null and b/.claude/skills/content-create-hero-image/assets/fonts/Inter-Regular.woff2 differ diff --git a/.claude/skills/content-create-hero-image/assets/fonts/MonaSans.woff2 b/.claude/skills/content-create-hero-image/assets/fonts/MonaSans.woff2 new file mode 100644 index 0000000000..fafba4840e Binary files /dev/null and b/.claude/skills/content-create-hero-image/assets/fonts/MonaSans.woff2 differ diff --git a/.claude/skills/content-create-hero-image/assets/fonts/README.md b/.claude/skills/content-create-hero-image/assets/fonts/README.md new file mode 100644 index 0000000000..d200a6db7c --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/fonts/README.md @@ -0,0 +1,16 @@ +# Brand fonts + +Static instances of the Eclipse brand fonts, bundled so covers render on-brand without a +`prisma/web` checkout. `scripts/export-png.sh` points fontconfig here automatically, and +`scripts/embed-fonts.py` subsets these into committed SVGs. + +| File | Family | Weight | Role | License | +| --------------------- | ---------- | ------ | ------------------------------------------------ | ------------------------------ | +| `MonaSans.woff2` | Mona Sans | 800 | Headings, display, product wordmark, big numbers | SIL OFL 1.1 (GitHub Mona Sans) | +| `Inter-Regular.woff2` | Inter | 400 | Body, subtitles, UI/category labels | SIL OFL 1.1 | +| `GeistMono.woff2` | Geist Mono | 500 | Data values, code, technical labels | SIL OFL 1.1 (Vercel Geist) | + +`MonaSans.woff2` is a static instance (weight 800, extended width) cut from Mona Sans VF +(`prisma/web` → `packages/eclipse/src/static/fonts/MonaSansVF[wdth,wght,opsz,ital].woff2`). +To regenerate at a different weight/width, instance the VF with `fonttools` and rename the +family to `Mona Sans`. All three families are open-source (OFL) and safe to redistribute. diff --git a/.claude/skills/content-create-hero-image/assets/hero1.svg b/.claude/skills/content-create-hero-image/assets/hero1.svg new file mode 100644 index 0000000000..afbb268c0a --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/hero1.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ROADMAP + // trajectory + + + + + + + + 2024 · Foundations + + + + 2025 · Prisma Postgres + + + + 2026 · Compute & AI + now ● + + + + + next · a bold new future + + + + + + + + + FIELD NOTES / 2026 + + + + + A bold + new future. + + + + Charting where Prisma goes next: + data, compute, and AI, built in the open. + + + Prisma + diff --git a/.claude/skills/content-create-hero-image/assets/hero2.svg b/.claude/skills/content-create-hero-image/assets/hero2.svg new file mode 100644 index 0000000000..f2db35dca7 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/hero2.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + schema.prisma + + + model User { +   id        String   @id @default(uuid()) +   email     String   @unique +   name      String? +   posts     Post[] +   createdAt DateTime @default(now()) + } + + + + + + + + + + + + + migration.sql + generated + + + database + in sync + + + + + PRISMA NEXT MIGRATIONS + + + + Schema to + migration + in one step + + + + Prisma + diff --git a/.claude/skills/content-create-hero-image/assets/hero3.svg b/.claude/skills/content-create-hero-image/assets/hero3.svg new file mode 100644 index 0000000000..e886b73eea --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/hero3.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + REQUEST-TIME IMAGE PIPELINE + + + + Image transforms + as app logic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + source + Bun.Image + webp + + + + + Prisma + diff --git a/.claude/skills/content-create-hero-image/assets/hero4.svg b/.claude/skills/content-create-hero-image/assets/hero4.svg new file mode 100644 index 0000000000..3dddb73f9f --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/hero4.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OPEN CHAT / + DURABLE STREAMING + + + + Chat that + never + drops a + token + + + + + Durable log backed + streaming + + + + + + + + + + + DURABLE LOG + head · offset 412 + + + + + + 409 + message.created + + + + + + 410 + message.delta + + + + + + 411 + message.delta + + + + + + + 412 + message.delta + + + + SSE tail + + + + + resumes at 412 + + + + + + + + client + + + + + Prisma + diff --git a/.claude/skills/content-create-hero-image/assets/logos/README.md b/.claude/skills/content-create-hero-image/assets/logos/README.md new file mode 100644 index 0000000000..f6d22bf7ba --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/README.md @@ -0,0 +1,36 @@ +# Logo assets + +Official, reusable brand marks for covers. Prefer these over re-extracting each time. + +## Sources + +- **Prisma logo + symbol** — official press kit [`prisma/presskit`](https://github.com/prisma/presskit). +- **Prisma Next mark** — the layered-prism brand mark, vectorized to white/dark. +- **Prisma Postgres icon** — FontAwesome [`chart-pyramid`](https://fontawesome.com/icons/chart-pyramid) (solid). +- **Prisma Compute icon** — FontAwesome [`microchip`](https://fontawesome.com/icons/microchip) (solid). + +## Inventory + +| File | What | Use | +| ---------------------------- | --------------------------------------------- | ------------------------------ | +| `prisma-logo-white.svg` | Official Prisma logo (mark + wordmark), white | Brand sign-off on dark covers | +| `prisma-logo-dark.svg` | Official Prisma logo, dark | Brand sign-off on light covers | +| `prisma-logo-indigo.svg` | Official Prisma logo, brand indigo | Indigo-on-light contexts | +| `prisma-symbol-white.svg` | Prisma prism symbol, white | Brand mark, dark covers | +| `prisma-symbol-indigo.svg` | Prisma prism symbol, indigo | Brand mark, light contexts | +| `prisma-next-mark-white.svg` | Prisma Next layered-prism mark, white | Next mark on dark | +| `prisma-next-mark-dark.svg` | Prisma Next mark, dark | Next mark on light | +| `prisma-next-logo.svg` | Next mark + "Prisma Next" (Mona Sans) | Prisma Next lockup | +| `prisma-postgres-icon.svg` | Chart-pyramid, teal `#71e8df` | Prisma Postgres covers | +| `prisma-compute-icon.svg` | Microchip, teal `#71e8df` | Prisma Compute covers | + +## Rules + +- **Intentional, not decorative.** One brand mark per cover. Product icons/lockups are for + product-focused covers where the product is the subject. +- Use the official `prisma-logo-*` / `prisma-symbol-*` and the Prisma Next mark as-is — do not + recolor (beyond the white/dark variants), stretch, rotate, or add effects. +- Product accents: Postgres = teal `#71e8df`, Compute = teal, Next pairs the white mark with a + Mona Sans wordmark, ORM = indigo `#4f46e5`. +- The lockup uses live Mona Sans text; render/commit it with the brand fonts embedded + (`scripts/embed-fonts.py`) so it stays correct standalone. diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-compute-icon.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-compute-icon.svg new file mode 100644 index 0000000000..df8dd4d80e --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-compute-icon.svg @@ -0,0 +1 @@ +Prisma Compute diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-logo-dark.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-logo-dark.svg new file mode 100644 index 0000000000..3b193c360a --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-logo-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-logo-indigo.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-logo-indigo.svg new file mode 100644 index 0000000000..df32bcfd46 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-logo-indigo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-logo-white.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-logo-white.svg new file mode 100644 index 0000000000..ed5aea1e88 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-logo-white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-next-logo.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-next-logo.svg new file mode 100644 index 0000000000..1dc9676c0b --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-next-logo.svg @@ -0,0 +1,15 @@ +Prisma Next logoPrisma Next diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-next-mark-dark.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-next-mark-dark.svg new file mode 100644 index 0000000000..a200d5a7cc --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-next-mark-dark.svg @@ -0,0 +1,15 @@ +Prisma Next diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-next-mark-white.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-next-mark-white.svg new file mode 100644 index 0000000000..a0ed85a9c1 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-next-mark-white.svg @@ -0,0 +1,15 @@ +Prisma Next diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-postgres-icon.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-postgres-icon.svg new file mode 100644 index 0000000000..74d171328f --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-postgres-icon.svg @@ -0,0 +1 @@ +Prisma Postgres diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-symbol-indigo.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-symbol-indigo.svg new file mode 100644 index 0000000000..86c7ab3706 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-symbol-indigo.svg @@ -0,0 +1,3 @@ + + + diff --git a/.claude/skills/content-create-hero-image/assets/logos/prisma-symbol-white.svg b/.claude/skills/content-create-hero-image/assets/logos/prisma-symbol-white.svg new file mode 100644 index 0000000000..669c6be273 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/logos/prisma-symbol-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/.claude/skills/content-create-hero-image/assets/templates/cover.svg b/.claude/skills/content-create-hero-image/assets/templates/cover.svg new file mode 100644 index 0000000000..4a142aacbd --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/templates/cover.svg @@ -0,0 +1,59 @@ + + + + {{TITLE_LINE_1}} {{TITLE_LINE_2}} + Prisma blog cover. {{SUBTITLE}} + + + + + + + + + + + + + + + + {{BADGE}} + + + + + {{TITLE_LINE_1}} + {{TITLE_LINE_2}} + {{TITLE_LINE_3}} + + {{SUBTITLE}} + + + + + + diff --git a/.claude/skills/content-create-hero-image/assets/tokens.json b/.claude/skills/content-create-hero-image/assets/tokens.json new file mode 100644 index 0000000000..9d52d3da30 --- /dev/null +++ b/.claude/skills/content-create-hero-image/assets/tokens.json @@ -0,0 +1,160 @@ +{ + "$comment": "Prisma blog/social cover design tokens. Color values are sourced from the Eclipse design system (prisma/web: packages/eclipse/src/styles/globals.css) and the canonical OG generator (apps/docs/src/app/og/[...slug]/route.tsx). Fonts and cover formats are cross-referenced with SOCIALS.fig. Treat this file as the single source of truth for generated covers; update it when Eclipse tokens change.", + "cover": { + "background": { + "$comment": "Signature Eclipse cover surface — a teal-green 'aurora': peak teal-green at the top (strongest top-left) fading through dark navy to near-black at the bottom. Built from a vertical base gradient PLUS a soft radial glow biased to the top-left, with a barely-there slate grid. This luminous teal top is the brand signature — never flatten it to a solid/near-flat dark. Layer order: bg gradient → aurora radial → grid.", + "gradientDirection": "vertical base (top → bottom) + top-left radial aurora", + "gradientTop": "#0C3B35", + "gradientUpperMid": "#0A1F23", + "gradientLowerMid": "#050E18", + "gradientBottom": "#03070D", + "aurora": { "type": "radial", "center": "cx 0.28, cy 0 (top, left-of-center)", "radius": 0.95, "stops": ["#1B6056 @0% opacity 0.6", "#0E433C @45% opacity 0.16", "transparent @100%"] }, + "grid": { "color": "#94A3B8", "opacity": 0.03, "cell": 48 }, + "lightAlt": "#ffffff" + }, + "accent": { + "$comment": "Mint teal #5EEAD4 (Eclipse teal-300) is the cover accent: eyebrows, headline highlight, hero numbers, hairlines. #71e8df is an accepted near-equivalent (docs OG).", + "teal": "#5EEAD4", + "altCyan": "#71e8df", + "glow": "rgba(45, 212, 191, 0.55)" + }, + "bars": { + "$comment": "Horizontal/vertical data bars. Neutral/reference (the 100% ceiling) = slate gradient; hero/product = teal gradient with glow; track behind = #1F2937.", + "track": "#1F2937", + "neutral": ["#64748B", "#CBD5E1"], + "hero": ["#14B8A6", "#5EEAD4"], + "heroGlow": "#2DD4BF" + }, + "card": { + "$comment": "Translucent panel (code block, log, terminal, comparison bars). A dark navy fill at ~0.74-0.8 opacity so the aurora shows faintly through; a hairline light border. Inner chips/boxes use #0B1220 @ ~0.7. Keep cards ≥48px off the canvas edge.", + "fill": "#0A1622", + "fillOpacity": 0.74, + "border": "rgba(255,255,255,0.10)", + "radius": 24, + "innerFill": "#0B1220" + }, + "text": { + "title": "#f7fafc", + "titleOnLight": "#111827", + "body": "#9CA3AF", + "muted": "#6b7280" + } + }, + "fonts": { + "$comment": "The Eclipse brand fonts (see prisma/web eclipse typography tokens). Headings/titles use Mona Sans VF (display); body uses Inter; data/code uses a mono. The skill bundles static instances in assets/fonts/ (Mona Sans 800, Inter 400, Geist Mono 500) so covers render on-brand without a prisma/web checkout. Do NOT use Barlow — that was the old docs OG face and reads off-brand. For an SVG that renders outside this repo, embed the faces with scripts/embed-fonts.py.", + "display": { + "family": "Mona Sans", + "role": "Titles, headings, product wordmark, big numbers", + "weight": 800, + "note": "Mona Sans VF; Eclipse titles run weight 800-900 with extended width (110-125). The bundled static instance is wght 800.", + "file": "MonaSans.woff2" + }, + "body": { + "family": "Inter", + "role": "Subtitles, descriptions, UI/category labels", + "weight": 400, + "file": "Inter-Regular.woff2" + }, + "mono": { + "family": "Geist Mono", + "alt": "Mona Sans Mono / JetBrains Mono", + "role": "Data values, code snippets, technical labels", + "weight": 500, + "file": "GeistMono.woff2" + } + }, + "fontsDir": "assets/fonts", + "typeScale": { + "$comment": "px values used by the canonical 1200x630 OG layout; scale proportionally for other formats.", + "title": 80, + "titleLong": 56, + "titleLongThreshold": 30, + "titleLineHeight": 1.2, + "description": 32, + "descriptionLineHeight": 1.5, + "badge": 24, + "wordmark": 22 + }, + "layout": { + "padding": 72, + "eyebrow": { + "$comment": "Brand eyebrow = a short teal hairline dash + uppercase letter-spaced label. Mona Sans 800, ~16px, letter-spacing ~2.4, teal #5EEAD4. The dash is a ~30px rounded line preceding the text.", + "dashLength": 30, + "gap": 14, + "fontSize": 16, + "letterSpacing": 2.4, + "uppercase": true + }, + "wordmark": { + "$comment": "Brand sign-off = the word 'Prisma' in Mona Sans 800, white, ~22px, in whichever bottom corner is clear of content (bottom-left when the diagram is right-side; bottom-right or top-right when content fills the bottom). NEVER the URL prisma.io/blog, and never an icon paired with extra text.", + "text": "Prisma", + "font": "Mona Sans 800", + "fontSize": 22, + "color": "#FFFFFF", + "position": "a bottom corner clear of content" + } + }, + "products": { + "$comment": "Per-product accent palettes from Eclipse. Postgres (ppg) and ORM have official token sets. Compute and Next are 2026 platform products without dedicated color tokens yet; they inherit the neutral/platform treatment (white label + teal accent). Update when official tokens ship.", + "postgres": { + "label": "Prisma Postgres", + "badge": "PRISMA POSTGRES", + "foreground": "#0d9488", + "foregroundStrong": "#0f766e", + "foregroundWeak": "#14b8a6", + "background": "#f0fdfa", + "backgroundStrong": "#ccfbf1", + "backgroundReverse": "#14b8a6", + "backgroundReverseStrong": "#0d9488", + "stroke": "#0d9488", + "reverseWeak": "#99f6e4", + "coverAccent": "#71e8df", + "icon": "logos/prisma-postgres-icon.svg" + }, + "orm": { + "label": "Prisma ORM", + "badge": "PRISMA ORM", + "foreground": "#4f46e5", + "foregroundStrong": "#4338ca", + "foregroundWeak": "#6366f1", + "background": "#eef2ff", + "backgroundStrong": "#e0e7ff", + "backgroundReverse": "#6366f1", + "backgroundReverseStrong": "#4f46e5", + "stroke": "#4f46e5", + "coverAccent": "#818cf8" + }, + "compute": { + "label": "Prisma Compute", + "badge": "PRISMA COMPUTE", + "$comment": "No official Eclipse color-token set yet; uses the brand chip icon and platform teal accent.", + "coverAccent": "#71e8df", + "icon": "logos/prisma-compute-icon.svg" + }, + "next": { + "label": "Prisma Next", + "badge": "PRISMA NEXT", + "$comment": "No official Eclipse color-token set yet; lockup pairs white 'Prisma' with teal 'Next' per the announcement treatment.", + "coverAccent": "#71e8df", + "lockup": "logos/prisma-next-logo.svg" + } + }, + "neutral": { + "foreground": "#111827", + "foregroundWeak": "#6b7280", + "foregroundWeaker": "#9ca3af", + "backgroundDefault": "#ffffff", + "background": "#f3f4f6", + "backgroundReverse": "#1f2937", + "backgroundReverseStrong": "#111827", + "stroke": "#e5e7eb", + "strokeStrong": "#d1d5db" + }, + "formats": { + "$comment": "Dimensions verified against prisma/web. 'hero' is the in-post image (heroImagePath). 'social' is the OG/preview image (metaImagePath). Filenames follow the blog repo convention: hero.(svg|jpg) and meta.png under /imgs/.", + "hero": { "width": 844, "height": 474, "ratio": "16:9", "use": "In-post hero (heroImagePath)", "file": "hero" }, + "social": { "width": 1200, "height": 630, "ratio": "1.91:1", "use": "OG / social preview (metaImagePath)", "file": "meta" }, + "youtube": { "width": 1280, "height": 720, "ratio": "16:9", "use": "YouTube thumbnail", "file": "youtube-thumbnail" }, + "blog": { "width": 1200, "height": 630, "ratio": "1.91:1", "use": "Generic blog cover (alias of social)", "file": "blog-cover" } + } +} diff --git a/.claude/skills/content-create-hero-image/references/design-review.md b/.claude/skills/content-create-hero-image/references/design-review.md new file mode 100644 index 0000000000..d5399fd366 --- /dev/null +++ b/.claude/skills/content-create-hero-image/references/design-review.md @@ -0,0 +1,107 @@ +# Design review (cover art) + +A structured design-critique pass for blog/social cover art. Adapted for **static SVG/PNG +covers** from the gstack `design-review` skill (a senior-designer-who-codes audit: +first-impression → categorized audit with A–F grades → atomic fix loop). The original audits +live web UIs for interaction/responsive quality; covers have no interactions, so those +categories are dropped and render-fidelity + thumbnail legibility are added. + +Run this in **step 8 of the workflow** before finalizing, and any time you refine an existing +cover. Critique the **rendered PNG**, not the SVG source — most defects are invisible in markup. + +## Three laws (apply to every cover) + +1. **Don't make me think.** The concept must be legible in ~3 seconds. If a viewer has to + decode the metaphor, the cover failed. +2. **Thinking is the cost, not pixels.** Minimize cognitive load — one idea, one accent, one + focal point. Every extra element taxes the reader. +3. **Omit, then omit again.** When in doubt, remove an element. Restraint is the house style. + +## Audit: grade each category A–F + +Start with a **first-impression gut reaction** (one honest sentence, before analysis: does it +look premium, or does it look like AI filler?). Then grade: + +| # | Category | What to check | +| -- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | **Hierarchy & focal point** | One clear focal point; eye lands where intended; eyebrow → headline → support → graphic order reads cleanly. | +| 2 | **Typography** | Mona Sans (display) / Inter (body) / Geist Mono (data) only — never Barlow or a fallback sans. Emphasis via colour or size, never random weights. Title hand-wrapped, not overflowing. | +| 3 | **Colour & contrast** | Only `tokens.json` values. Text passes a squint legibility test on the dark surface. One accent (teal/indigo), not a rainbow. | +| 4 | **Spacing & alignment** | Even canvas margins; elements share a grid; consistent padding inside cards; nothing kissing an edge. | +| 5 | **Composition & balance** | Left-anchored headline with right-side breathing room (or a single centred graphic). Diagram cards sit ≥ 48px off the canvas edge. Visual weight balanced L↔R. | +| 6 | **Metaphor clarity** | The diagram tells one true story; arrows connect real nodes; every flow ends in a labelled recipient; header numbers match the data they label. | +| 7 | **Render fidelity** | No stripped whitespace in code (`xml:space="preserve"`), no missing-glyph boxes, no fallback fonts, no text overflow or clipping, no overlapping nodes. | +| 8 | **Thumbnail legibility** | Still readable shrunk to ~200px wide (the real feed/SERP size). Squint: headline + metaphor survive. | +| 9 | **Brand correctness** | Logos intentional, correctly coloured, not stretched, not wallpaper; no `prisma.io/blog` on canvas; product accent correct (Postgres = teal, ORM = indigo, Compute/Next = platform teal). | +| 10 | **AI-slop check** | None of the anti-patterns below. | + +A cover ships at **B or better in every category** — no C-or-lower anywhere. + +## AI-slop anti-patterns (auto-fail any present) + +Adapted from gstack's slop list: + +- Purple/rainbow gradients; glows on everything. +- Faux-3D blobs, glassmorphism soup, drop-shadow overload. +- Icon-in-a-circle grids; uniform bubbly corner radius everywhere. +- Everything centred (covers are left-anchored unless a single graphic owns the canvas). +- Emoji used as a design element. +- Generic hero copy ("Unlock the power of…", "Supercharge your…", "The future of…"). +- Stock illustrations, literal robots, mascots. +- Decorative tagline filler with periods ("Resize. Encode. Cache."). + +## Fix loop + +For each finding, worst-first: + +1. **Locate** the element in the `hero.svg`. +2. **Fix minimally** — smallest change that resolves it; don't redesign around a small flaw. +3. **Re-render** the PNG and **re-inspect** — confirm the fix landed and broke nothing else. +4. **Classify**: `verified` (looks right in the raster), `best-effort` (improved but + constrained), or `reverted` (fix made it worse — roll back). +5. Re-grade the affected categories. Repeat until every category is ≥ B. + +Then regenerate the committed PNG so it matches the SVG, and re-run `embed-fonts.py` if fonts +aren't yet embedded. + +## Verify alignment by measurement, not by eye + +Alignment/centering claims must be **measured against the rendered raster**, not judged by +eye — a few pixels of drift is invisible at a glance but obvious to the author, and "looks +centered in my render" is not "is centered". For any centering or alignment a reviewer +asserts is fixed, prove it with pixel coordinates: + +```bash +# Trim the ink inside a region and report its bounding box + offset within the crop. +# Compare the ink center to the intended geometry (chip center, row center, card center). +magick meta.png -crop x++ +repage -fuzz 35% -trim \ + -format "ink %wx%h at +%X+%Y (center x=%[fx:+page.x+w/2] y=%[fx:+page.y+h/2])" info: +``` + +- Use a low-ish `-fuzz` (~30–40%) so dim secondary text (greys) is kept as ink, not trimmed + as background; too high a fuzz silently drops it and gives a false center. +- Measure connector legs, bar extents, and the vertical drop the same way (they're font-free + paths, so they render identically everywhere) and confirm legs land on the elements they + point at. +- A claim ships only when the measured center matches the intended center within ~1px. + +**Renderer-portable centering.** `text-anchor="middle"` on a single text run centers +identically in every renderer (librsvg, browsers, Figma). `text-anchor="middle"` combined +with a child `` can drift between renderers — so for a label that pairs a bold +name with a quiet annotation (e.g. `api` + `Hono`), prefer **two single-run centered lines +stacked** on the same x over one mixed-run line with `dx`. It is both more portable and reads +as obviously centered (the bold word sits dead-center instead of left-of-block). + +## Report + +When asked for a review (not just a fix), report concisely: + +```text +First impression: +Grades: hierarchy A · type A · colour B · spacing A · composition B · metaphor A · + render A · thumbnail B · brand A · slop pass +Findings → fixes: + 1. [composition, C→A] card touched right edge → pulled to 72px margin (verified) + 2. [render, F→A] code spaces stripped → added xml:space="preserve" (verified) +Result: ships. +``` diff --git a/.claude/skills/content-create-hero-image/references/design-system.md b/.claude/skills/content-create-hero-image/references/design-system.md new file mode 100644 index 0000000000..d86ff389a1 --- /dev/null +++ b/.claude/skills/content-create-hero-image/references/design-system.md @@ -0,0 +1,169 @@ +# Prisma cover design system + +The visual language for Prisma blog and social covers, distilled from the Eclipse +design system, the canonical OG generator, and `SOCIALS.fig`. Machine-readable +values live in [`../assets/tokens.json`](../assets/tokens.json) — that file is the +source of truth; this document explains intent. + +## Sources + +| What | Where (in `prisma/web`) | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Color tokens (hex) | `packages/eclipse/src/styles/globals.css` | +| Type tokens (font + scale) | `apps/eclipse/content/design-system/tokens/typography.mdx` | +| Layout composition recipe | `apps/docs/src/app/og/[...slug]/route.tsx` (layout only — it uses Barlow, which is **off-brand**; covers use Mona Sans) | +| Brand fonts | `packages/eclipse/src/static/fonts/` (Mona Sans VF, Mona Sans Mono VF); bundled static instances in [`../assets/fonts/`](../assets/fonts/) | +| Logos / marks | Official press kit [`prisma/presskit`](https://github.com/prisma/presskit); product icons in [`../assets/logos/`](../assets/logos/) (`README.md`) | +| Blog cover conventions | `apps/blog/content/blog//index.mdx` + `apps/blog/public//imgs/` | +| Hand-designed cover library | `SOCIALS.fig` (see [`figma-source.md`](./figma-source.md)) | + +The skill bundles its brand fonts in `assets/fonts/`, so it renders on-brand without extra setup. +This skill lives in the `prisma/web` repo, so the live Eclipse tokens in `packages/eclipse/` are +available to reconcile against. + +## The look in one paragraph + +A premium **teal-green "aurora" surface** — a luminous teal glow at the top (strongest top-left) +fading through dark navy to near-black at the bottom — with a single mint-teal accent (`#5EEAD4`). +A **dash-prefixed eyebrow** (a short teal hairline + uppercase label) names the topic; a bold, +**large** Mona Sans headline with its key phrase in teal carries the message; and one **content +module** does the showing — a row of icon tiles, a comparison card of bars, a log/data panel, a +terminal, or a code card. The word **`Prisma`** signs off in a clear bottom corner. Nothing else: +no stock illustration, no random gradient, no decorative noise. Restraint is the brand, and the +diagram is minimal — favour a few clean boxes and one hero glyph over arrows, chips, and pills. +(See the worked examples in `assets/examples/`, which mirror the brand reference frames.) + +## Color + +- **Cover surface (default, dark):** a **vertical** gradient — teal-green `#163535` (top) → `#0A1018` → near-black `#03060E` (bottom). The lighter top carries the eyebrow + headline; the bottom anchors the content module. This visible top-to-bottom shift is part of the look — do not flatten it. +- **Accent (signature):** mint teal **`#5EEAD4`** (Eclipse teal-300) for eyebrows, headline highlight, hero numbers, hairlines (`#71e8df` is an accepted near-equivalent). Use exactly one accent per cover. +- **Title text:** `#f7fafc`/white on dark, `#111827` on light. +- **Body / subtitle / muted:** `#9CA3AF` (`#a0aec0` ok). +- **Background texture:** a faint slate grid (`#94A3B8` @ ~4.5%, 48px cells) plus a soft teal radial glow (`rgba(45,212,191,0.16)`), positioned to complement the composition. Both barely-there — atmosphere, not decoration. +- **Bar / data fills:** neutral/reference bars = slate gradient `#64748B → #CBD5E1`; the hero/product bar = teal gradient `#14B8A6 → #5EEAD4` with a `#2DD4BF` glow. Track behind bars = `#1F2937`. +- **Card surface:** `#111827` at ~92% opacity, `1.5px` border `#FFFFFF` @ 8%, radius ~22. +- **Product palettes** (semantic, from Eclipse): Postgres = teal (`#0d9488` / cover accent `#5EEAD4`), ORM = indigo (`#4f46e5`). Compute and Next inherit the platform teal. See `tokens.json → products`. + +Light covers are allowed for editorial/educational pieces: white background, `#111827` +title, product-colored accent. Dark is the default and the launch/announcement standard. + +## Typography + +| Role | Family | Weight | Notes | +| ------------------ | ------------------------------------ | ------ | ------------------------------------------------------------- | +| Title / headline | **Mona Sans** | 800 | Eclipse titles run 800–900, extended width; line-height ~1.1 | +| Eyebrow (kicker) | **Geist Mono** | 600 | UPPERCASE, letter-spacing ~2.7, accent teal, leading `—` rule | +| Subtitle / body | **Inter** | 400 | line-height 1.5 | +| Data values / code | **Geist Mono** (alt: Mona Sans Mono) | 500 | benchmark numbers, req/s, snippets, API paths | +| Big numbers | **Mona Sans** | 800 | hero stats and percentages | + +Never substitute a different display face. **Mona Sans + Inter** is the pairing — **not Barlow**. +Barlow is the legacy docs-OG face and reads off-brand; do not use it on covers. + +### Fonts & rendering (important) + +The brand faces are **Mona Sans** (headings/display, weight 800), **Inter** (body, 400), and +**Geist Mono** (data/code, 500). The skill bundles static instances in +[`../assets/fonts/`](../assets/fonts/) (Mona Sans is a variable font; the bundled instance is the +weight-800 display cut). Use those families/weights — an arbitrary weight or a different family +makes renderers fall back to a generic sans, the #1 cause of an off-brand cover. + +**One source of truth: embed the fonts, then render.** A cover that only _names_ the families +renders with fallback fonts wherever they are not installed. Two renderers disagree about +fonts, which is the #1 cause of "the PNG and SVG look different": + +- A **browser/Figma** renders the SVG's embedded `@font-face` (correct brand fonts). +- **librsvg/`rsvg-convert`** _ignores_ `@font-face`, and since fontconfig does not index the + bundled `.woff2`, it falls back to a generic sans (e.g. Verdana) — wrong. + +So the pipeline is: **(1) `scripts/embed-fonts.py `** inlines the faces as base64 +`@font-face` (self-contained SVG), then **(2) `scripts/export-png.sh`** renders the PNG with +**headless Chrome/Chromium**, which honors the embedded faces — making the PNG pixel-identical +to the SVG-in-browser. Always embed before exporting or sharing. (rsvg/magick remain only as +fallbacks and warn that fonts may not match.) + +## Layout (canonical 1200×630) + +- Uniform **72px** padding on all edges; nothing touches the canvas edge. +- **Headline block:** top-left; eyebrow, then title (1–3 hand-wrapped lines), optional one-line + subtitle (~20px gap). Large type — title ~62–72px. +- **`Prisma` wordmark:** the brand sign-off — the word `Prisma` in Mona Sans 800, white, ~22px, + in whichever **bottom corner is clear of content** (bottom-left when the module is right-side; + bottom/top-right when content fills the bottom). Never the URL `prisma.io/blog`; never an icon + paired with extra text. +- Strong left alignment and generous negative space on the right. Do not center everything. + +### Spacing & breathing room (coherence) + +Cramped elements read as "off". Hold these minimums and keep them consistent across a set: + +- **≥ 48px** between any card/diagram and the canvas edge. +- **≥ 40px** between sibling tiles/boxes in a row (equal gaps; symmetric about their center). +- **≥ 24px** padding inside a card before its content; **≥ 28px** between stacked rows of content. +- Eyebrow → headline → subtitle → module each get clear air; never let two text blocks crowd. +- Verify spacing on the **rendered PNG**, by measurement, not by eye (see `design-review.md`). + +### Shared anatomy (every cover) + +The house style is one frame with a few fixed parts and a swappable **content module**: + +1. **Eyebrow** top-left — a short teal **hairline dash** (~30px) + uppercase label in **Mona Sans + 800** ~16px, letter-spacing ~2.4, accent teal (`— LABEL`). Names the topic (e.g. + `REQUEST-TIME IMAGE PIPELINE`, `THROUGHPUT VS RAW PG`). +2. **Headline** below it — **Mona Sans 800**, **~62–72px** (go large), tight tracking (about -1.8), + 1–3 lines, white with the **key phrase in accent teal** (e.g. "Image transforms / **as app + logic**"). +3. **Content module** in the lower/right area — the visual that carries the idea (see catalog). + Keep it **minimal**: a few clean tiles/rows and one hero element beat a busy diagram. +4. **`Prisma` wordmark** — Mona Sans 800, white, ~22px, in a clear bottom corner. Always present. +5. **Base** — the teal-green **aurora** surface (bright teal top, strongest top-left → near-black + bottom) + a barely-there slate grid. Always. Never a flat/near-solid dark fill. + +Left-anchored, generous space, one accent, no decoration beyond the base aurora. + +### Content modules (pick one per cover) + +Each example folder in [`../assets/examples/`](../assets/examples/) is a worked instance: + +| Module | When | Looks like | Example | +| -------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | +| **Pipeline / flow** | a sequence or transform (A → B → C) | a row of rounded **square tiles**, equal size and equal gaps, a mono label under each; the key tile gets a teal-glow border and holds the hero logo/glyph (e.g. the Bun mark). Keep it minimal — the tiles + labels read as a flow; skip arrows, param chips, and pills | `image-transformations-with-bun-on-prisma-compute` | +| **Comparison card** | "X vs Y", "% of", benchmarks | rounded `#111827`@92% card of **horizontal bars** — each row = prism icon + label, big right-aligned number + unit; reference row fills 100% (slate), hero row fills its true ratio (teal + glow). Bar width _is_ the comparison | `prisma-next-benchmark` | +| **Data / log panel** | streams, logs, events, real-time | a dark panel of monospace **rows** (timestamp + label + mini-bar), optionally a small flow beneath (`source → client`) | `building-open-chat` | +| **Terminal card** | CLI, scaffolding, getting-started | a terminal-style card with a `$ command` and a short ✓ checklist | `create-prisma-deploy-prisma-compute` | +| **Code card** | config, schema, API shape | a code-editor card (filename tab + syntax-tinted lines), optional target nodes beneath | `prisma-compute-config-file` | + +Tiles/cards: rounded ~22–24px, **translucent** fill `#0A1622` @ ~0.74 (so the aurora shows +faintly through), hairline border `#FFFFFF` @ ~10%; inner chips/boxes `#0B1220` @ ~0.7; the hero +element uses a teal border + `#2DD4BF` glow. Labels and code/data are **Geist Mono**; headings and +big numbers are **Mona Sans**; prose is **Inter**. + +**No-copy covers (common).** Most Prisma blog images carry **no headline at all** — the content +module plus the `Prisma` wordmark do the work. Default to no-copy unless the post has a short, +punchy thesis worth setting as a headline. A no-copy cover centres/enlarges the module and keeps +the eyebrow optional. + +**Isometric skew (optional hero treatment).** For card/table/grid modules, a subtle isometric +skew (e.g. `transform="matrix(1,0.18,-0.18,1,…)"` or a ~15–25° rotate + scale) gives the hero a +premium 3-D plane. Use it for **cards and tables**; keep **flow/loop/pipeline and "→ bill" +diagrams flat** (skew muddies a left-to-right read). One skewed plane per cover, never the whole +canvas. + +**Multiple directions.** When asked for N directions (1–4), produce N independent +`cover-.svg`/`.png` pairs exploring distinct modules or framings (e.g. literal diagram vs +typographic thesis vs comparison), each fully on-brand, and present them together for selection. + +**Graphic-led variant.** When a single chart/diagram is the whole message, you can drop the +headline and **center the module**, with the standalone product logo (e.g. `prisma-next-logo.svg`) +centered on top as the only brand mark. + +## Formats + +| Format | Dimensions | Frontmatter / use | File name | +| ------------------ | ---------- | ----------------- | ----------------------- | +| In-post hero | 844×474 | `heroImagePath` | `hero.svg` / `hero.png` | +| Social / OG | 1200×630 | `metaImagePath` | `meta.png` | +| YouTube thumbnail | 1280×720 | video | `youtube-thumbnail.png` | +| Generic blog cover | 1200×630 | alias of social | `blog-cover.svg/.png` | + +Scale font sizes proportionally when the frame changes (e.g. ×0.70 for the 844-wide hero). diff --git a/.claude/skills/content-create-hero-image/references/figma-mcp.md b/.claude/skills/content-create-hero-image/references/figma-mcp.md new file mode 100644 index 0000000000..636d512a2f --- /dev/null +++ b/.claude/skills/content-create-hero-image/references/figma-mcp.md @@ -0,0 +1,93 @@ +# Refreshing from the live Figma workspace (optional) + +When a **Figma MCP is connected**, pull current specs and assets straight from the Prisma +team workspace instead of relying on the checked-in extraction. This keeps `tokens.json` +and `assets/logos/` in sync as the brand evolves. When no Figma MCP is connected, skip +this entirely — the committed assets and `prisma/web` source are the offline-safe default. + +## How the Figma MCP is exposed here + +The Figma Plugin API runs through the **`use_figma`** tool, which is gated behind the +`figma-use` skill. Before any `use_figma` call: load `figma-use`, and pass `figma-use` in +its `skillNames` parameter. Work read-only and in small steps. Scripts use plain JS with +top-level `await` and `return` (the return value is the only output channel). + +The snippets below use the `use_figma` query helpers — `node.query(selector)` (CSS-like +selectors), `.values([...])`, and `.first()` — documented in the `figma-use` skill. These +are `use_figma` conveniences layered on the Plugin API, not raw `figma.*` calls; the raw +equivalents are `findAll`/`findOne` with a predicate. + +Point it at the **SOCIALS** file (or the current covers file) — ask the operator for the +Figma URL, or operate on the file they have open. A design file URL looks like +`figma.com/design/...`. + +## 1. Refresh design tokens from Figma variables + +Read the Eclipse color variables live and reconcile against `assets/tokens.json`. Only +overwrite values that genuinely changed; keep the file's structure and comments. + +```js +// use_figma (read-only) — list collections, then dump brand/ppg/orm color variables +const cols = await figma.variables.getLocalVariableCollectionsAsync(); +const toHex = (c) => '#' + ['r','g','b'].map(k => Math.round(c[k]*255).toString(16).padStart(2,'0')).join(''); +const out = []; +for (const col of cols) { + for (const id of col.variableIds) { + const v = await figma.variables.getVariableByIdAsync(id); + if (v.resolvedType !== 'COLOR') continue; + if (!/brand|ppg|orm|background|foreground|stroke/i.test(v.name)) continue; + const modeId = col.modes[0].modeId; + let val = v.valuesByMode[modeId]; + // resolve one level of alias if present + if (val && val.type === 'VARIABLE_ALIAS') { + const a = await figma.variables.getVariableByIdAsync(val.id); + val = a.valuesByMode[Object.keys(a.valuesByMode)[0]]; + } + if (val && typeof val === 'object' && 'r' in val) out.push({ name: v.name, hex: toHex(val) }); + } +} +return out; +``` + +Compare the returned `{name, hex}` list to `tokens.json` and update changed hex values. +Figma colors are 0–1; convert to hex as above. + +## 2. Use the live cover frames as composition reference + +Find the current blog/social cover frames to confirm dimensions and layout before designing. + +```js +// use_figma (read-only) — current page; fan out per page if needed (one setCurrentPageAsync per call) +return figma.currentPage + .query('FRAME[name^=blog], FRAME[name*=COVER], FRAME[name*=SOCIAL]') + .values(['name', 'width', 'height']); +``` + +Take a `get_screenshot` (or `await node.screenshot()`) of a representative frame for a +visual reference. Match the live layout; do not copy a one-off frame's content. + +## 3. Export logo / mark nodes as SVG + +Refresh `assets/logos/` from the source nodes. Export as **`SVG_STRING`** (returns markup +you can save directly); avoid binary `PNG`/`SVG` byte arrays in the return channel. + +```js +// use_figma (read-only) — export a named node's vector as an SVG string +const node = figma.currentPage.query('[name=Prisma Logo], [name*=wordmark]').first(); +if (!node) return { error: 'node not found' }; +return { name: node.name, svg: await node.exportAsync({ format: 'SVG_STRING' }) }; +``` + +Save the returned `svg` string to the matching file under `assets/logos/`, then regenerate +its PNG (if needed) with `scripts/export-png.sh`. Keep the white/dark variants in sync by +recoloring the `fill`. + +## Guardrails + +- **Read-only by default.** This skill consumes the workspace; it does not edit it. Never + run write scripts against the shared Prisma file. +- **Reconcile, don't clobber.** Treat `tokens.json` as the source of truth and apply only + real diffs, preserving its `$comment` notes. +- **Stay offline-safe.** Everything here is optional. If `use_figma` is unavailable or the + operator has no file URL, fall back to the committed assets and the `prisma/web` source + (see [`design-system.md`](./design-system.md) and [`figma-source.md`](./figma-source.md)). diff --git a/.claude/skills/content-create-hero-image/references/figma-source.md b/.claude/skills/content-create-hero-image/references/figma-source.md new file mode 100644 index 0000000000..010b6643fb --- /dev/null +++ b/.claude/skills/content-create-hero-image/references/figma-source.md @@ -0,0 +1,60 @@ +# SOCIALS.fig — source reference + +`SOCIALS.fig` is Prisma's master Figma file for blog, social, and banner cover art. +It is the design source the hand-made blog hero/meta images are cut from, and the +reference this skill matches. + +## Why it is not committed + +The file is ~98 MB (mostly embedded raster photography). This repo has no Git LFS and +its largest tracked asset is < 1 MB, so committing the binary would bloat every clone +permanently. Instead, the **lightweight, relevant content** is extracted into this skill +(tokens, fonts, formats, logos, board thumbnail) and the binary is parked at +`spaces/SOCIALS.fig` (gitignored) for local re-extraction. + +If `spaces/SOCIALS.fig` is missing, ask the operator for the latest export from the +Prisma Figma workspace before re-extracting. + +## What the file contains + +- Sections: `BLOG`, `BLOG_LANDING`, `BANNERS`, `COVER`, plus per-post cover frames + (e.g. `blog-6.19`, `blog-614-article`, `blog_PrismaNextTS_01`, `blog-2025-year`). +- Eclipse color variables: `Color/Brand/100–900`, `Brand/brand-primary`, and the + `background/foreground/stroke` `-ppg` semantic tokens (teal Prisma Postgres scale). +- Fonts seen in older frames: Barlow (display), Inter, JetBrains/Geist/Roboto Mono. + **Note:** the current Eclipse brand display font is **Mona Sans** (see + [`design-system.md`](./design-system.md) → Typography) — covers use Mona Sans, not Barlow. + Body is Inter; data/code is Geist Mono. +- `assets/figma/board-thumbnail.png` — a small overview render of the whole board. +- `assets/figma/board-meta.json` — the file's export metadata. + +## How the extraction was done (re-extraction recipe) + +A local `.fig` is a ZIP container. The vector canvas is a kiwi message whose data block +is **Zstandard**-compressed (Figma format version ≥ 106). + +```bash +unzip -o spaces/SOCIALS.fig -d /tmp/fig # -> canvas.fig, thumbnail.png, meta.json, images/ +# canvas.fig = 8-byte 'fig-kiwi' header + uint32 version + [uint32 len + block] x N +# block 0 = kiwi schema (raw deflate); block 1 = data (zstd) +python3 - <<'PY' +import struct +d=open('/tmp/fig/canvas.fig','rb').read(); pos=12 +n0=struct.unpack(' [--fonts ] + + defaults to the bundled assets/fonts directory. fonttools + brotli are optional +(`pip install fonttools brotli`) and make smaller SVGs. Idempotent: skips a file that +already has an @font-face block. +""" +import argparse +import base64 +import io +import re +import sys +from pathlib import Path + +FACES = [ + ("Mona Sans", 800, "MonaSans.woff2"), + ("Inter", 400, "Inter-Regular.woff2"), + ("Geist Mono", 500, "GeistMono.woff2"), +] + + +def find_fonts_dir(explicit: str | None) -> Path: + if explicit: + return Path(explicit) + # the skill bundles its brand fonts next to this script + cand = Path(__file__).resolve().parent.parent / "assets" / "fonts" + if cand.is_dir(): + return cand + sys.exit("error: no --fonts dir given and no bundled assets/fonts found") + + +def used_chars(svg: str) -> str: + # collect text inside /, ignore markup; add a safe punctuation set + chars = set("0123456789%,/.()-:· ") + for m in re.findall(r"<(?:text|tspan)[^>]*>([^<]*) None: + ap = argparse.ArgumentParser() + ap.add_argument("svg") + ap.add_argument("--fonts") + args = ap.parse_args() + + try: + from fontTools import subset # noqa: F401 + import brotli # noqa: F401 + except ImportError: + subset = None + else: + from fontTools import subset + + svg_path = Path(args.svg) + svg = svg_path.read_text() + if "@font-face" in svg: + print(f"{svg_path}: already has embedded fonts — skipping") + return + + fonts = find_fonts_dir(args.fonts) + text = used_chars(svg) + rules = [] + for family, weight, fname in FACES: + font_path = fonts / fname + if not font_path.is_file(): + sys.exit(f"error: bundled font not found: {font_path}") + if subset: + opts = subset.Options() + opts.flavor = "woff2" + opts.desubroutinize = True + font = subset.load_font(str(font_path), opts) + ss = subset.Subsetter(options=opts) + ss.populate(text=text) + ss.subset(font) + buf = io.BytesIO() + subset.save_font(font, buf, opts) + font_bytes = buf.getvalue() + else: + font_bytes = font_path.read_bytes() + b64 = base64.b64encode(font_bytes).decode() + rules.append( + f" @font-face{{font-family:'{family}';font-style:normal;" + f"font-weight:{weight};src:url(data:font/woff2;base64,{b64}) format('woff2');}}" + ) + style = " \n" + + if "" in svg: + svg = svg.replace("\n", "\n" + style, 1) + else: + svg = re.sub(r"(]*>\n)", r"\1 \n" + style + " \n", svg, 1) + svg_path.write_text(svg) + mode = "subset" if subset else "full-font fallback" + print(f"{svg_path}: embedded {', '.join(f for f, _, _ in FACES)} via {mode} ({len(svg)} bytes)") + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/content-create-hero-image/scripts/export-png.sh b/.claude/skills/content-create-hero-image/scripts/export-png.sh new file mode 100755 index 0000000000..dbc460b126 --- /dev/null +++ b/.claude/skills/content-create-hero-image/scripts/export-png.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# Export a cover SVG to a pixel-exact PNG in Prisma's brand fonts. +# +# Usage: +# export-png.sh +# +# Example (social/OG): +# export-png.sh hero.svg meta.png 1200 630 +# +# Fonts: covers use Mona Sans (display/headings), Inter (body) and Geist Mono (data/code). +# This skill bundles those families in assets/fonts, and the script points +# fontconfig at them for the render. To render with a different set, point +# FONT_DIR at a folder of font files. +set -euo pipefail + +IN="${1:?input svg required}" +OUT="${2:?output png required}" +W="${3:?width required}" +H="${4:?height required}" + +[ -f "$IN" ] || { echo "error: input not found: $IN" >&2; exit 1; } + +# --- resolve a fonts directory -------------------------------------------- +# Use the skill's bundled brand fonts (Mona Sans, Inter, Geist Mono). Override with FONT_DIR. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +: "${FONT_DIR:=}" +if [ -z "$FONT_DIR" ] && [ -d "$SCRIPT_DIR/../assets/fonts" ]; then + FONT_DIR="$SCRIPT_DIR/../assets/fonts" +fi + +FC_CONF="" +if [ -n "$FONT_DIR" ] && [ -d "$FONT_DIR" ]; then + FC_CONF="$(mktemp)" + cat >"$FC_CONF" < + + + $FONT_DIR + /etc/fonts/fonts.conf + $(mktemp -d) + +EOF + export FONTCONFIG_FILE="$FC_CONF" + echo "fonts: using $FONT_DIR" >&2 +fi + +# --- warn only when no brand fonts directory was resolved ------------------ +# (fc-list does not reliably list .woff2 from a temp config, so per-family +# checks give false negatives; instead trust a resolved FONT_DIR.) +if [ -z "$FONT_DIR" ] || [ ! -d "$FONT_DIR" ]; then + echo "warning: no brand-fonts directory found; Mona Sans/Inter/Geist Mono will fall back to a generic sans. Set FONT_DIR to a folder of font files." >&2 +fi + +# --- render ----------------------------------------------------------------- +# Prefer a Chromium-based browser. It honors the SVG's embedded @font-face, so the +# PNG matches EXACTLY how the SVG renders in a browser/Figma (real Mona Sans/Inter/ +# Geist Mono). librsvg ignores @font-face and, when the brand families are not +# installed as .ttf/.otf, silently falls back to a generic sans — which is why PNGs +# used to look different from the SVGs. Embed fonts (embed-fonts.py) BEFORE exporting +# so Chrome has the faces to use. rsvg/magick remain as fallbacks (warned). +ABS_IN="$(cd "$(dirname "$IN")" && pwd)/$(basename "$IN")" + +find_chrome() { + if [ -n "${CHROME_BIN:-}" ] && [ -x "$CHROME_BIN" ]; then echo "$CHROME_BIN"; return 0; fi + local c + for c in \ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + "/Applications/Chromium.app/Contents/MacOS/Chromium" \ + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" \ + google-chrome-stable google-chrome chromium chromium-browser; do + [ -x "$c" ] && { echo "$c"; return 0; } + command -v "$c" >/dev/null 2>&1 && { command -v "$c"; return 0; } + done + return 1 +} +CHROME="$(find_chrome || true)" + +if [ -n "$CHROME" ]; then + if ! grep -q "@font-face" "$IN"; then + echo "warning: $IN has no embedded @font-face — run embed-fonts.py first or the browser render will use fallback fonts." >&2 + fi + echo "render: chrome headless (embedded fonts, browser-accurate) — $CHROME" >&2 + # Optional supersampling: SCALE=2 renders at 2x then downsamples for crisper text. + # Default SCALE=1 writes the target size directly (atomic, no resize step). + SCALE="${SCALE:-1}" + # Headless Chrome occasionally hangs; bound each attempt with a hard timeout and retry + # so the export is reliable (no stuck processes, no stale half-written files). + TIMEOUT="${CHROME_TIMEOUT:-30}" + rm -f "$OUT" + for attempt in 1 2; do + PROFILE="$(mktemp -d)" + ( "$CHROME" --headless=new --disable-gpu --no-sandbox --hide-scrollbars \ + --user-data-dir="$PROFILE" --force-device-scale-factor="$SCALE" \ + --window-size="${W},${H}" --virtual-time-budget=3000 \ + --default-background-color=00000000 \ + --screenshot="$OUT" "file://$ABS_IN" >/dev/null 2>&1 ) & + CPID=$! + ( sleep "$TIMEOUT"; kill -9 "$CPID" 2>/dev/null ) & TPID=$! + wait "$CPID" 2>/dev/null || true + kill "$TPID" 2>/dev/null || true + rm -rf "$PROFILE" + [ -s "$OUT" ] && break + echo "warning: chrome render attempt $attempt produced no output; retrying" >&2 + done + if [ ! -s "$OUT" ]; then + echo "error: chrome render produced no output after retries" >&2; exit 1 + fi + if [ "$SCALE" != "1" ] && command -v magick >/dev/null 2>&1; then + magick "$OUT" -resize "${W}x${H}" "$OUT" + fi +elif command -v rsvg-convert >/dev/null 2>&1; then + echo "warning: rendering with rsvg-convert — it ignores embedded @font-face; PNG may use fallback fonts and differ from the SVG. Install Chrome/Chromium for brand-accurate output." >&2 + rsvg-convert -w "$W" -h "$H" "$IN" -o "$OUT" +elif command -v magick >/dev/null 2>&1; then + magick -background none -density 300 "$IN" -resize "${W}x${H}!" "$OUT" +elif command -v inkscape >/dev/null 2>&1; then + inkscape "$IN" --export-type=png --export-filename="$OUT" -w "$W" -h "$H" +else + echo "error: no SVG renderer found (need Chrome/Chromium, rsvg-convert, magick, or inkscape)" >&2 + exit 1 +fi + +[ -n "$FC_CONF" ] && rm -f "$FC_CONF" +echo "wrote $OUT ($(magick identify -format '%wx%h' "$OUT" 2>/dev/null || echo "${W}x${H}"))" diff --git a/.claude/skills/content-write-blog/SKILL.md b/.claude/skills/content-write-blog/SKILL.md new file mode 100644 index 0000000000..248247b836 --- /dev/null +++ b/.claude/skills/content-write-blog/SKILL.md @@ -0,0 +1,174 @@ +--- +name: content-write-blog +description: Use when the operator wants to write a blog post, draft a blog article, start a new post for the Prisma blog, or publish to prisma.io/blog. +metadata: + version: "2026.6.15" +--- + +# Write Blog Post + +Produce a _skeleton_ for a Prisma blog post — frontmatter, section headings, and short stubs — not a finished article. The human author writes the prose. + +This skill produces content only. It does **not** check out, branch, commit, push, or open pull requests against the blog repository. The operator owns the repository workflow: they prepare their own checkout of the blog repo, place or accept the skeleton into it, and push and open the pull request themselves, conforming to that repository's contribution process. Keep the boundary clear at every step. + +The blog is owned by DevRel but contributed to by everyone, so do not assume the operator is on the DevRel team or familiar with the blog repository. Treat first-time contributors as the default case. + +## Assets + +- `assets/positioning.md` — Prisma's internal positioning doc. It is **not committed to this public repo** (it's gitignored); obtain it from the internal positioning source and place it at `assets/positioning.md` before drafting. Read it before writing the lead and section stubs so the angle, framing, and product claims stay aligned with how Prisma describes itself. It is the source of truth for positioning, so do not contradict it. If it is absent, ask the operator for the current positioning before grounding the post. + +## Asking the operator questions + +When this skill needs a decision with a small set of options (which existing author to use, whether a proposal looks right, where the checkout lives), prefer a structured-choice question if the environment supports one. Fall back to plain questions when it does not, or when the answer is genuinely freeform (a pitch, a slug, a bio). Bias toward reasonable defaults from context; ask only when multiple valid paths exist. + +## Step 1: Gather the pitch + +The operator must provide direction for the post. Valid inputs: + +- A pitch pasted into the conversation (a few sentences explaining the angle, audience, and key takeaway) +- A path to an existing draft markdown file +- Both — a pitch plus a reference draft + +If nothing was provided, ask: _"What's the blog post about? Share a pitch (angle, audience, key takeaway), or a path to a draft if you have one."_ + +Do not proceed without input, and do not invent a pitch on the operator's behalf. + +## Step 2: Ground in positioning + +Read the positioning asset above. Note the product names, claims, and framing that the post must stay consistent with. If the pitch conflicts with positioning, surface the conflict to the operator before drafting — do not silently "correct" the pitch, and do not write copy that contradicts positioning. + +## Step 3: Locate the blog repository checkout + +The operator prepares and owns this checkout — this skill does not create it. Ask the operator for the path to their local checkout of the blog repository (the `prisma/web` repo, which contains the blog content). + +- If the operator provides a path, confirm it is the expected repository by inspecting its contents (top-level layout, a blog content directory). If it looks like the wrong repository, halt and surface the mismatch — do not modify or relocate it. +- If the operator does not have a checkout ready, ask them to prepare one and provide the path. Without a checkout, you cannot discover conventions or place files; you may still draft the skeleton inline in the conversation for the operator to paste in, but flag that conventions are unverified. + +Treat the checkout as read-and-write for content files only. Never perform repository-level operations (branching, committing, pushing, or opening pull requests) — those belong to the operator. + +## Step 4: Survey blog conventions + +Do not hardcode conventions in this skill — discover them from the operator's checkout each time by reading files: + +1. Locate the blog posts directory by inspecting the repo layout (look for `content/blog/`, `posts/`, `src/content/blog/`, or similar). +2. Read 2–3 of the most recent posts. Extract: frontmatter fields used, filename / slug convention, where the date lives (filename vs. frontmatter), title casing, lead-paragraph style. +3. Read the authors directory (commonly `authors/`, `content/authors/`, or referenced from a recent post's frontmatter). Note the author profile file shape and required fields. +4. Read any contributing guide, README, or blog-specific contributor doc in the repo. Note required steps, formatting expectations, and review expectations — the operator will need these when they push. +5. Note the repository's formatting / lint step if its package configuration declares one, so you can apply it to the files you write. + +Briefly summarise to the operator what you found (one or two sentences) before continuing. + +## Step 5: Resolve the author + +Ask the operator for their author slug (e.g. `jane-doe`). + +- If a profile already exists in the authors directory under that slug, use it as-is. +- If no profile exists, this is a first-time contributor step. Ask for the fields required by the discovered author file shape (typically name, bio, role, social handles, avatar path). Scaffold the profile file alongside the post. Flag this to the operator so they know to verify the bio reads correctly and to include it when they push. + +If the operator is unsure which slug is theirs, list the existing author slugs and ask them to pick or confirm a new one. + +## Step 6: Propose slug, filename, date, and frontmatter + +Based on the pitch and the conventions from Step 4: + +- **Slug**: lowercase, hyphenated, derived from the working title. Keep it short and searchable. +- **Filename**: match the repo's convention (e.g. `YYYY-MM-DD-slug.md`, `slug/index.mdx`, `slug.md`). +- **Date**: today's date, in the format used by recent posts. +- **Frontmatter**: populated with title, slug, date, author slug, and any other fields recent posts use (tags, description, etc.). Where a field needs human judgement (description, tags), pre-fill a reasonable guess and clearly flag it as a placeholder. + +Present the proposal as a single block and confirm before writing any files. Do not proceed without confirmation. + +## Step 7: Write the skeleton article + +Write the post file at the location decided in Step 6, inside the operator's checkout. The file contents: + +1. **Title** — H1 (or per repo convention; some setups derive the title from frontmatter only — match what recent posts do). +2. **Lead** — 1–2 sentences drafted from the pitch and grounded in positioning. Intentionally short; the human will rewrite. +3. **Section headings** — 3–5 `##` headings inferred from the pitch, each followed by a one-line italic stub: _"Write about X here — touch on Y and Z."_ +4. **Closing** — a brief CTA stub (a single placeholder sentence, e.g. _"Wrap up with the takeaway and a clear next step for the reader."_). + +If you also scaffolded an author profile in Step 5, write that file too. + +After writing, apply the repository's formatting / lint step (discovered in Step 4, if any) to the new files and fix violations, so the operator inherits clean files. + +## Step 8: Add contextual links + +After the skeleton is written, enhance it with relevant links before handing it back to the operator. Add only links that genuinely help the reader — never pad the post with low-value or tangential references. + +### Internal Prisma links + +Scan the skeleton for opportunities to link to Prisma's own content. Only add a link when the post introduces a topic and the linked page lets the reader go meaningfully deeper: + +- **Prisma product names (link the first mention to docs).** The first time the post names **Prisma Postgres**, **Prisma Compute**, or **Prisma Next**, link it inline to that product's documentation. These internal doc links build SEO and point the reader to the authoritative reference. Link the first mention only; do not re-link later occurrences. Use the canonical docs page, or a more specific docs page when one fits the sentence better: + - Prisma Postgres → `https://www.prisma.io/docs/postgres` + - Prisma Compute → `https://www.prisma.io/docs/compute` + - Prisma Next → `https://www.prisma.io/docs/orm` (Prisma Next is the next-generation Prisma ORM) +- **Related blog posts (cross-link by topic).** When the skeleton mentions a topic an existing post covers in depth, link to that post. Search the blog content directory by topic and tag, not just by exact title, so you catch matches the wording doesn't make obvious. For example, a mention of a Postgres bloom filter or bloom index should link to the bloom-index post (`postgres-bloom-index-the-overlooked-postgres-feature`); a mention of `LISTEN`/`NOTIFY` should link to the post that covers it. These cross-links keep readers on the blog and strengthen internal SEO. +- **Documentation** — link to relevant docs pages on `prisma.io/docs` when the skeleton mentions a feature, concept, or workflow beyond the three products above that the docs explain authoritatively. +- **Product pages** — for a high-level, positioning-level mention of a product, the marketing page (`prisma.io/postgres`, `prisma.io/compute`, `prisma.io/orm`) can fit better than docs. Prefer docs for technical mentions; use the product page only for whole-product framing. + +Discover available internal links from the blog repository checkout (Step 3). Inspect existing blog posts in the content directory for relevant earlier articles, matching on topic and tags. If the checkout includes docs or the operator can point to a docs checkout, inspect its structure for matching pages. For product pages, use Prisma's known site structure. + +### External links + +Scan the skeleton for mentions of external products, frameworks, libraries, languages, or standards. Add a link only when: + +- The mention is substantive — the post depends on the reader knowing what the thing is, or the linked resource directly extends the point being made. A passing mention is not a link opportunity. +- The link points to the project's official site or canonical documentation — never to a third-party tutorial, Wikipedia, or unofficial resource. +- The URL is stable and permanent — avoid links to specific blog posts that may age, versioned docs that will rot, or redirect/affiliate URLs. + +### Return a link inventory + +After adding links, compile a numbered list of every link added, split into two sections. Present this inventory to the operator so they can review and remove any links they consider unnecessary. Do not remove links unilaterally after presenting — the operator decides. + +Format: + +``` +**Internal links added:** + +1. [anchor text](url) — placed in "Section Heading", context: why this link is relevant +2. … + +**External links added:** + +1. [anchor text](url) — placed in "Section Heading", context: why this link is relevant +2. … +``` + +### Quality bar + +If fewer than three meaningful link opportunities exist, do not force links. A sparse but relevant link set is better than a dense one that reads like a link farm. Zero links is acceptable when the skeleton does not naturally invite any. + +## Step 9: Hand-off summary + +End by giving the operator, in a short block: + +- The path to the post file (within their checkout). +- The author profile path, if one was scaffolded. +- A reminder that the repository workflow is theirs: create a feature branch, commit the skeleton (and author profile), push, and open the pull request following the blog repo's contribution process. The pull request should be opened as a **draft** until the post is fleshed out. +- A clear next-step instruction: _"Edit the post to flesh out each section, keep it consistent with positioning and free of AI slop (see Writing quality), then branch, commit, push, and open a draft PR per the blog repo's contributing guide."_ + +Do not claim the post is "done" or "ready" — it is a skeleton handed back to the operator. + +## Writing quality + +The lead you draft, and the prose the author later writes over your stubs, ship to prisma.io/blog as public content. Draft the lead to this bar, and name the same bar in the hand-off so the author holds it: + +- **Open with the point.** Skip "Here's the thing", "It turns out", "In today's world". The first sentence says what the post is about. +- **Name the actor.** A person or a product does the thing. Write "the team shipped the fix that week", not "the fix happened". +- **Be specific.** Replace "this changes everything" or "the implications are significant" with the concrete change. Drop "every", "always", and "never" where a real number fits better. +- **Cut filler.** No "really", "simply", "just", and no marketing verbs like "leverage", "unlock", "seamless", "powerful". +- **Say it straight.** "Not X, but Y" and "X isn't the problem, Y is" telegraph the reversal. State Y. +- **Vary sentence length**, and use a comma, colon, or period in place of em dashes. + +## Anti-patterns + +- **Writing finished prose.** This skill produces a skeleton. Long generated paragraphs defeat the point and dilute the author's voice. Stubs and section headings only. +- **Performing the repository workflow.** This skill does not branch, commit, push, or open pull requests. Producing those is the operator's job — stop at writing content into their checkout and handing off. +- **Hardcoding blog conventions.** The repo evolves. Always discover frontmatter shape, slug rules, blog directory, author shape, and the formatting step from the operator's checkout in Step 4. +- **Contradicting positioning.** Read the positioning asset first. Do not write claims or framing that conflict with how Prisma describes itself; surface conflicts instead of papering over them. +- **Skipping the author profile step for first-time contributors.** The missing profile is a common trip-wire. Surface it and scaffold it alongside the post. +- **Inventing the pitch.** If the operator gave no direction, ask. Do not generate a topic from imagination. +- **Over-linking.** Adding links to every proper noun, tangential reference, or vaguely related page produces a link farm, not a useful post. Link only when the reader genuinely benefits. If in doubt, leave it out — the operator can always add links later. The one exception is the first mention of Prisma Postgres, Prisma Compute, and Prisma Next: always link those to docs for SEO. +- **Linking to non-canonical sources.** External links must point to official sites and canonical docs. Wikipedia, Medium, third-party tutorials, and versioned URLs are not acceptable link targets. +- **Modifying or relocating the operator's checkout.** If the checkout is the wrong repo or has unexpected state, surface it and ask — do not clean up, move, or reset it unilaterally. diff --git a/.claude/skills/docs-writer/README.md b/.claude/skills/docs-writer/README.md new file mode 100644 index 0000000000..8b8e1d6004 --- /dev/null +++ b/.claude/skills/docs-writer/README.md @@ -0,0 +1,29 @@ +# docs-writer + +How to write Prisma documentation a developer can follow without getting stuck. Read this before you write or edit a page in `apps/docs` or `apps/blog`. + +## Writing style in one minute + +You don't have to read the whole guide to contribute. Follow these and your page is already most of the way there: + +1. **One page, one task.** Pick a single thing the reader will accomplish. Split anything bigger. +2. **Say what, then why, then how.** Before every command, one sentence on what it does and why it matters. +3. **Show one real example,** not a list of every option. Use real code that runs as written. +4. **Give a way to verify.** End with a command the reader runs to confirm it worked, and the output they should see. +5. **Name the products precisely.** Prisma Postgres, Prisma Compute, Prisma Next. Show how they connect, don't describe each in isolation. +6. **Cut the hype.** No "powerful", "seamless", "unlock". The reader already chose Prisma; they want it to work. + +That's the whole philosophy. `SKILL.md` is the detailed version. + +## The products you're documenting + +- **Prisma Postgres**: managed PostgreSQL in the Prisma platform. +- **Prisma Compute**: serverless TypeScript hosting that runs next to Prisma Postgres. +- **Prisma Next**: the TypeScript-native Prisma ORM (schema, client, migrations). + +They're designed to work together: an app on Prisma Compute queries Prisma Postgres through Prisma Next. Make that connection the spine of a page, not a footnote. + +## Using it + +- **With Claude Code:** the guide loads as a skill automatically. Ask it to "write a how-to for X" or "make this page clearer." +- **Without Claude Code:** read [`SKILL.md`](SKILL.md) as a checklist while you write, and [`references/how-to-use.md`](references/how-to-use.md) for a worked example of each page type. diff --git a/.claude/skills/docs-writer/SKILL.md b/.claude/skills/docs-writer/SKILL.md new file mode 100644 index 0000000000..13d2cdd685 --- /dev/null +++ b/.claude/skills/docs-writer/SKILL.md @@ -0,0 +1,148 @@ +--- +name: docs-writer +description: Use when writing, rewriting, or improving technical docs (quickstarts, how-tos, tutorials, concept pages, or API references). +metadata: + author: Prisma + version: "2026.6.23" +--- + +# Docs Writer + +Write documentation a developer can follow from start to finish without getting stuck or guessing. Every page answers three questions, in this order: what am I doing, why does it matter, and what do I do next. + +This skill is opinionated. If a rule here conflicts with an existing house style, follow the house style and tell the operator about the conflict. + +For a step-by-step example of writing each kind of page (how-to, concept, reference) and rewriting an existing one, see `references/how-to-use.md`. + +## Foundation + +These come from Prisma's positioning. They shape how docs frame the product, without turning a page into marketing. + +- **Prisma is one integrated platform, not three separate tools.** Prisma Next, Prisma Postgres, and Prisma Compute are built to work together for TypeScript teams. When a page spans more than one, show how they connect; the integration is the reason to use them together, so make it the spine of the page rather than a footnote. +- **Write for a TypeScript developer, often one building with an AI coding agent.** They want fewer moving parts and a working path, not a tour of features. +- **Be honest about maturity.** Don't claim general availability or production readiness for early-stage products (Prisma Compute is early-stage). Don't call a product best-in-class or claim feature parity across products. When the source material is unsure, use narrower wording and flag it. + +## Before you write + +Answer these four questions. If you can't, find the answer before drafting. + +- **Who is the reader, and what do they already know?** A backend developer new to this product needs different framing than someone migrating from a competitor. Write for one reader. +- **What one task does this page complete?** One page, one task. If you're documenting two tasks, write two pages and link them. +- **What does the reader need set up before step 1?** List exact prerequisites: account, runtime version, installed CLI, env vars. +- **How will the reader know it worked?** Decide the success signal now. You'll end the page with it. + +## Page shape + +Use this structure for a how-to or quickstart. Drop any section that doesn't apply. Don't add sections for ceremony. + +1. **Title**: name the task as a verb phrase. "Deploy a Prisma Compute app", not "Deployment". +2. **Intro (1-2 sentences)**: state what the reader will have done by the end, and the one reason it matters. +3. **Prerequisites**: a bulleted list with exact versions and links. The reader should clear this list in under a minute. +4. **Numbered steps**: see "Writing a step". +5. **Verify**: a command the reader can copy and run, with the exact expected output. "You should see your app respond" is not a verification; `curl https://your-app.url` returning `{"status":"ok"}` is. +6. **Next steps**: one to three links to the obvious follow-on tasks. + +A numbered step must require the reader to do something: run a command, edit a file, click a button. If there's no action, it's a note, not a step. Put it inline as a short callout instead of numbering it. + +## Other page types + +A how-to gets the reader through a task. Two other types come up often. Same voice, same slop rules; different spine. + +**Concept page** explains one idea so the reader can make decisions. + +1. One-sentence definition of the concept in plain terms. +2. Why it matters: the problem it solves or the mistake it prevents. +3. How it works, at the level of detail the reader needs to act, and no deeper. +4. When to use it (and when not to). +5. A link to the how-to that applies it. A concept page should hand off to a task. + +**Reference page** is for a reader who already knows what they want and needs exact details. + +- Lead with the signature, syntax, or endpoint. +- List every parameter with type, whether it's required, and the default. +- Give one runnable example per entry. +- No narrative. The reader is scanning, not reading. + +## Improving an existing page + +When the input is a page that already exists, triage before you rewrite. + +1. Name the page type and the one task or idea it serves. If it serves more than one, split it first. +2. Read it as the target reader and mark every place you'd get stuck: a missing prerequisite, a command with no context, an unexplained result, a claim you can't verify. +3. Fix in this order, because each unblocks the reader more than the next: + 1. Missing prerequisites or setup that make later steps fail. + 2. Steps with no context, wrong commands, or no way to confirm success. + 3. Hidden limitations and claims you can't verify. + 4. Slop and tone. +4. Change structure and wording freely. Don't change technical facts you can't confirm; flag them instead. + +## Writing a step + +Every step follows the same rhythm. Don't skip parts of it. + +1. **One sentence of context before any code.** Say what the step does and why. + - Good: "Install the Prisma CLI so you can run migrations from your terminal." + - Weak: "Run the following command." (Why? What does it do?) +2. **The exact command or code.** It must run as written. Label code blocks with the file path when the reader edits a file. +3. **What the reader should see.** Show real output, a created file, or a screen. Readers use this to confirm they're on track. +4. **A short explanation of the result**, only when it isn't obvious from the output. +5. **The likely failure and its fix**, when a step commonly breaks. One line: "If you see `EADDRINUSE`, another process is using the port; stop it or set `PORT`." This is what makes a doc feel hand-held instead of hopeful. + +### Example: a weak step rewritten + +Weak: + +> **Initialize Prisma** +> Run `npx prisma init`. + +Better: + +> **Initialize Prisma** +> Set up Prisma in your project. This creates a `prisma/` directory with your schema file and a `.env` file for your database connection string. +> +> ```terminal +> npx prisma init +> ``` +> +> You now have `prisma/schema.prisma` and a `.env` file. Open `.env` and confirm `DATABASE_URL` is present. + +## Concrete rules + +- Address the reader as "you". Use the imperative for actions: "Run", "Open", "Add". +- Put the requirement or context before the command, never after. +- Every code block must run or compile as written. No pseudo-code in step-by-step instructions. +- Label code blocks with the target file path when the reader edits or creates a file. +- Prefer one complete, real example over an abstract description of options. Show the common case; link to the reference for the rest. +- State a limitation where the reader will hit it, inline with the relevant step, not in a footnote. +- Link the first mention of a concept the reader may not know. Don't re-link it every time. +- Use exact product names: Prisma Postgres, Prisma Compute, Prisma Next. Don't shorten "Prisma Postgres" to "the database" or "Prisma Next" to "the ORM" once a page covers more than one product. +- When you tell the reader something is automatic, show the trigger that makes it happen and how to confirm it did. "Compute injects `DATABASE_URL` automatically" needs a follow-up: "Run `prisma compute env` to confirm it's set." + +## Cut the slop + +Delete these on sight. They add length, not clarity. + +- **Throat-clearing openers**: "Here's the thing", "It turns out", "The truth is", "Let me walk you through". State the content directly. +- **Meta-commentary**: "In this section, we'll…", "As we'll see…", "Now, let's…". Just write the section. +- **Filler adverbs**: really, just, simply, actually, basically, of course. ("Simply run" insults a stuck reader.) +- **Business jargon**: leverage, unlock, seamless, robust, powerful, deep dive, game-changer. Replace with the plain verb or cut. +- **Vague value claims**: "a powerful experience", "the future of X", "everything you need". Replace with a specific capability or remove. +- **Em dashes**. Use a comma, colon, or period instead. +- **Hype**: don't sell inside docs. The reader already chose the product; they want it to work. + +## Voice + +- Calm and direct. The reader is mid-task, not browsing a landing page. +- Accuracy over persuasion. If behavior is uncertain or version-dependent, say so and say how to check. +- Name limitations honestly and early. Hiding them costs the reader more later. + +## Final pass + +Before you finish, check: + +- [ ] Could a reader with only the listed prerequisites complete every step? +- [ ] Does every command run as written, with no missing setup? +- [ ] Does each step say what it does before showing the command? +- [ ] Is there a way to verify success at the end? +- [ ] Did you cut every phrase from "Cut the slop"? +- [ ] Are product names and limitations accurate? diff --git a/.claude/skills/docs-writer/references/how-to-use.md b/.claude/skills/docs-writer/references/how-to-use.md new file mode 100644 index 0000000000..db6df5f508 --- /dev/null +++ b/.claude/skills/docs-writer/references/how-to-use.md @@ -0,0 +1,67 @@ +# How to use docs-writer + +The skill handles four jobs. Each starts the same way: say what you want and which page type, then follow the steps. Every job ends with the "Final pass" checklist in `SKILL.md`. + +The examples below lean on the products you'll usually document: an app on **Prisma Compute** that queries **Prisma Postgres** through **Prisma Next**. + +| You want to... | Page type | Jump to | +| --------------------------------------- | --------- | ------------------------------ | +| Get a reader through a task | How-to | [Write a how-to](#write-a-how-to) | +| Explain an idea so a reader can decide | Concept | [Write a concept page](#write-a-concept-page) | +| Give exact details for a known API | Reference | [Write a reference page](#write-a-reference-page) | +| Fix a page that already exists | Any | [Improve an existing page](#improve-an-existing-page) | + +--- + +## Write a how-to + +**Prompt:** "Write a how-to for deploying a TypeScript API to Prisma Compute that reads from Prisma Postgres with Prisma Next." + +1. Answer the four "Before you write" questions: who the reader is, the one task, the prerequisites, the success signal. +2. Lay out the page shape: title, intro, prerequisites, numbered steps, verify, next steps. +3. Write each step in order: a context sentence, the exact command, the expected output, and the likely failure with its fix. +4. Make the product connection the spine: the Prisma Next client connects to Prisma Postgres, and the app runs on Prisma Compute. Show that, don't just mention it. +5. End with a Verify section the reader can copy and run, like a `curl` against the deployed URL. +6. Run the Final pass checklist; cut everything in "Cut the slop". + +Result: a page a reader can follow start to finish, confirm it worked, and know what to do next. + +--- + +## Write a concept page + +**Prompt:** "Explain connection pooling in Prisma Postgres." + +1. Open with a one-sentence definition in plain terms. +2. Say why it matters: the problem it solves or the mistake it prevents (for example, an app on Prisma Compute exhausting database connections under load). +3. Explain how it works, only as deep as the reader needs to make a decision. +4. State when to use it and when not to: the pooled connection string for app queries, the direct one for migrations. +5. Link to the how-to that applies the idea, so the page hands off to a task. + +Result: a reader who can decide whether and when the concept applies to them. + +--- + +## Write a reference page + +**Prompt:** "Document the `cacheStrategy` option on a Prisma Next query." + +1. Lead with the signature or syntax. +2. List every parameter: type, required or optional, and the default. +3. Give one runnable example per entry. +4. Cut the narrative. The reader is scanning, not reading. + +Result: a page a reader can scan to find the exact detail they came for. + +--- + +## Improve an existing page + +**Prompt:** "Make this Prisma Postgres caching guide clearer and more practical." + +1. Name the page type and the one task or idea it serves. If it serves more than one, split it first. +2. Read it as the target reader and mark every place you'd get stuck. +3. Fix in priority order: broken setup first, then broken steps, then hidden limitations, then slop and tone. +4. Keep technical facts you can't confirm; flag them instead of changing them. + +Result: the same content, reordered and trimmed so a reader stops getting stuck. diff --git a/.gitignore b/.gitignore index 23c74905af..c10da36d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ opensrc # locally cloned prisma-next repo prisma-next/ + +# internal positioning doc — not user-facing, keep out of this public repo +.claude/skills/content-write-blog/assets/positioning.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b06a60ddbc..ff091afaa0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -132,3 +132,14 @@ PR titles should follow the same convention as commits: - Prefer clarity over cleverness. - Avoid drive-by refactors inside feature/bugfix PRs. - Follow existing patterns within each app. + +--- + +## Writing documentation + +Writing or editing a page in `apps/docs` or `apps/blog`? Start with the [`docs-writer` guide](.claude/skills/docs-writer/README.md). The "Writing style in one minute" section at the top is all you need to make a solid first contribution; the rest is there when you want it. + +It covers how we write docs for Prisma Postgres, Prisma Compute, and Prisma Next: page shape, how to write a step, what to cut, and a final-pass checklist, with a worked example for each kind of page. + +- **Using Claude Code?** The guide loads as a skill automatically. Ask it to write or improve a page. +- **Not using Claude Code?** Read [`SKILL.md`](.claude/skills/docs-writer/SKILL.md) as a checklist and [`how-to-use.md`](.claude/skills/docs-writer/references/how-to-use.md) for examples.