ScribekitDocs

/scribekit-hero

Reference for the /scribekit-hero skill: its six modes, the three-files model, the six gradients, and the output shape for every surface.

11 min readUpdated 12 August 2026
/scribekit-hero

/scribekit-hero generates hero images for a project's blog posts, docs pages, and its README. Every hero is rendered deterministically from HTML/CSS via headless Chrome - never an AI image generator and never a stock photo. All heroes share one family: white text on a full-bleed gradient, with a badge and eyebrow, an auto-fit H1, a subtitle, and an optional byline.

The skill is self-contained. It carries its own design assets in assets/ and depends on no other skill. Every mode starts by learning the current project (brand tokens, site font, logo, assets dir, and whether the surface is multi-language) so nothing about any project is baked in.

Invoke the skill

/scribekit-hero [blog-hero|regenerate-heroes|docs-hero|regenerate-docs-heroes|readme-hero|tune-gradients] [slug | blog|docs|readme]

The skill has no -- flags. The only argument after the mode is the mode's target. Never run two modes in one invocation; an ambiguous verb gets one clarifying question first.

Modes

ModeArgumentWhat it does
blog-hero<slug|path>Create one blog post's hero, or update an existing one. No target lists the content dir and asks.
regenerate-heroesnoneRe-render every blog hero from its saved params.
docs-hero<slug|path>Create one docs page's hero, or update an existing one. No target lists the docs content dir and asks.
regenerate-docs-heroesnoneRe-render every docs hero from its saved params.
readme-herononeCreate or update the README/banner hero.
tune-gradients[blog|docs|readme]Review and adjust that surface's gradient palette. Tunes the blog palette when the argument is omitted.

The regenerate-* modes exist for design changes: edit the component or a hero.settings.js, re-run, and every hero picks the change up with zero per-page edits. Neither touches frontmatter, because the image: paths are unchanged.

The three files

Design, brand, and content are separated so a design change never means re-editing posts. Each hero is built from exactly three files.

FileScopeContents
Component - assets/hero.jsShips with the skillThe reusable hero design: structure, base styles, auto-fit, readiness. Never recoloured, and copied into a project only as the documented last-resort fork (see below).
Settings - hero.settings.jsOne per surfacebrand (font, badge/logo including badge.bare, accent, eyebrow, texture, css, default byline) and gradients. The usual place a project customises how heroes look.
Params - <slug>/hero.jsOne per heroPure data: the gradient pick, title, and subtitle, plus an optional byline. Saved so any hero is reproducible.

Settings are seeded from assets/hero.settings.template.js, which exports brand and gradients. Where the file lives depends on the surface:

SurfaceSettings path
Blog<content-dir>/hero.settings.js
Docs<docs-content-dir>/hero.settings.js
README.agentstore/scribekit-hero/readme/hero.settings.js

Customising the component

Three seams, in the order you should reach for them. Stop at the first that does the job.

  1. brand.* in hero.settings.js. The font, the badge or logo, the accent, the eyebrow, the texture, the default byline. Covers most projects.
  2. brand.css - a raw CSS string appended after the component's own stylesheet, so it wins at equal specificity over the component's selectors (.stage, .content, .eyebrow, .badge, .title, .subtitle, .byline). This is the seam for anything the brand fields do not express, and it stays inside the settings file.
  3. hero.component.js beside the surface's hero.settings.js - a full copy of assets/hero.js, edited freely. The render step uses it in place of the shipped component. This is a fork: that surface stops receiving upstream fixes to auto-fit, readiness, and capture timing, permanently. Last resort.

Two traps worth knowing before you touch the component at all:

  • When the skill resolves through node_modules, assets/ is read-only. The next npm install reverts your edit silently, and regenerate-heroes then renders heroes missing whatever you added. Use seam 2 or 3 instead.
  • A backtick anywhere in BASE_CSS, even inside a comment, breaks the module and renders a blank #0e0e11 image with no error. Run node --check assets/hero.js after editing it.

A params file carries the gradient name plus the text, and nothing else. The eyebrow comes from hero.settings.js and is never repeated per hero:

export default {
    gradient: "aurora-glow",                 // the rotated gradient name
    title:    "The exact page title",        // auto-fit shrinks long ones
    subtitle: "One tight supporting line",
};

Docs heroes omit the byline: DocMeta has no author field. Blog heroes may define one; any field left out is filled from the settings brand.byline.

The gradients

Six gradients are defined in hero.settings.js. A hero uses one by name.

Name
radial-mesh
diagonal-ribbon
aurora-glow
soft-sweep
horizon-glow
veil

Rotation rule. A new post or page picks gradients[(number of existing pages) mod gradients.length].name, so index 0 is radial-mesh, 1 is diagonal-ribbon, and so on. The pick is deterministic; there is no random selection. Count distinct page folders (<content-dir>/<slug>/), including hidden pages, and count each slug once - a page keeps its gradient across every language, because there is one params file per page. Updating an existing hero keeps its gradient unless the user is restyling.

A README hero has no rotation: it uses its own gradient value, defaulting to the first gradient.

Output shape per surface

The two shapes are never conflated.

SurfaceShapeDefault formatSizeRounding
Blog, docsOpaque rectangleJPEG, ~80 KB1200x630Never baked in. The site rounds and borders it in CSS.
READMERounding baked into transparent cornersPNG1200x630 by defaultBaked in at 10px by default. GitHub strips README CSS, so the <img> cannot be rounded there. The format must hold an alpha channel, so JPEG is invalid for this surface.

The README image is downscaled with a plain sips -z H W resize, which keeps the format PNG and preserves the transparent corners. Its size and radius are optional exports on the README hero.settings.js (export const size = { w: 1200, h: 400 }, export const radius = 10), never edits to the shared component. Keep h no smaller than about 420 so the auto-fit title does not clip.

Choosing the file format

Each surface's hero.settings.js may export an optional format, which decides the encoder and the saved file's extension for every hero on that surface:

export const format = "webp";   // "jpg" | "png" | "webp" | "avif"

Omit it for the surface default: jpg for blog and docs, png for the README.

formatEncoded withNotes
jpgsips -z 630 1200 -s format jpeg -s formatOptions 82 <out>.png --out <dest>.jpgOpaque; no alpha.
pngsips -z 630 1200 <out>.png --out <dest>.pngKeeps the transparent rounded corners.
webpmagick <out>.png -resize 1200x630 -quality 82 <dest>.webpRoughly 30-50% smaller than the JPEG; keeps alpha.
avifmagick <out>.png -resize 1200x630 -quality 55 <dest>.avifSmallest; keeps alpha; slowest to encode.

sips only reads WebP and AVIF, so those two need ImageMagick (brew install imagemagick). When magick is missing, the skill falls back to the surface's default format, saves the hero anyway, and tells you. The image: front-matter always carries the extension of the file actually written.

Heroes double as OG/social cards. Most crawlers handle WebP, but AVIF support is still spotty - some show no preview at all, and GitHub does not render it reliably in a README. Pick avif only when the hero is not also the social card.

Changing format on a surface that already has heroes means re-running regenerate-heroes / regenerate-docs-heroes, re-wiring each image: to the new extension, and deleting the old files.

Output paths

SurfaceRendered image
Blog<assets>/blog/<slug>/hero.<code>.<ext>
Docs<assets>/docs/<slug>/hero.<code>.<ext>
README.agentstore/scribekit-hero/readme/hero.<ext>

<ext> is the surface's format (see above): jpg unless it says otherwise, png for the README.

Every language is named by its code, the default included: hero.en.jpg, hero.fr.jpg, matching the page files en.mdx and fr.mdx.

Localised heroes

The hero bakes in the page's text, so every translation gets its own rendered image, and a translation's image: must never point at another language's file. There is still one params file per page, not one per language. <slug>/hero.js exports a (locale) => params function:

// <docs-content-dir>/<slug>/hero.js - one file, all languages.
const text = {
    en: { title: "Configure a Docs instance", subtitle: "Wire your docs content dir in one file." },
    fr: { title: "Configurer une instance Docs", subtitle: "Reliez votre dossier docs en un fichier." },
};
export default (locale = "en") => ({ gradient: "aurora-glow", ...text[locale] });

The gradient is the single gradient value in the shared file, so it is the same across every language and no re-rotation is possible or needed. The render loops the configured locales, one image each. If a configured locale returns no text, the run stops and the params file is fixed, because that language would render a blank title.

How image: is wired

The rendered hero is wired into each language's front-matter as always a site-root path with a leading slash, never a bare filename and never a full URL:

image: "/assets/docs/<slug>/hero.en.jpg"

For docs this is DocMeta.image, the page's OG/social-card image. The docs page component does not render it inline. OG emits DocMeta.image as-is under metadataBase and JSON-LD absolutizes it, so only a leading-slash root path is correct on both. Updating an existing hero also bumps updated: (from date +%F, quoted). See Docs guide for the rest of the front-matter contract.

Files

FilePurpose
SKILL.mdThe router and the shared design foundation: Step 0, the three-files model, customising the component, auto-create settings, the 6 gradients, tune-gradients, the render pipeline, guardrails.
blog.mdblog-hero and regenerate-heroes.
docs.mddocs-hero and regenerate-docs-heroes.
readme.mdreadme-hero: size and radius, the transparent rounded output, README wiring.
assets/hero.jsThe reusable hero component. Exports renderHero and renderGallery.
assets/hero.settings.template.jsThe seed copied to each surface as hero.settings.js. Exports brand and gradients.
assets/hero.host.htmlSingle-hero render host. Tokens {{HERO_COMPONENT}}, {{HERO_SETTINGS}}, {{HERO_PARAMS}}.
assets/hero.gallery.htmlThe tune-gradients gallery host. Two tokens.

Constraints

  • Only hero.settings.js carries brand, colour, size, and radius. A project's colours are never hardcoded into the shared assets/hero.js; the component is copied into a project only as the deliberate hero.component.js fork, and never edited in place under node_modules.
  • The output shape per surface is never conflated: blog and docs are opaque JPEG rectangles with no baked rounding; the README is a rounded transparent PNG.
  • Every hero is a local asset, never a remote URL.
  • Committed files are never modified by a render. The host HTML is filled into a throwaway scratchpad copy, and that copy is what gets screenshotted.
  • A render touches only hero assets, the surface's hero.settings.js and hero.js params, and (blog and docs) the page's image: and updated: front-matter, or (README, on the user's OK) README.md.
  • Never start a dev server. Never create git branches.

The render pipeline

The claude-in-chrome MCP blocks file://, so the render runs headless Chrome from the CLI (/Applications/Google Chrome.app/Contents/MacOS/Google Chrome on macOS, google-chrome or chromium on Linux):

"<chrome>" --headless=new --hide-scrollbars --force-device-scale-factor=2 \
  --window-size=<W>,<H> --virtual-time-budget=6000 --default-background-color=00000000 \
  --allow-file-access-from-files --user-data-dir="<fresh temp dir>" \
  --screenshot="<out>.png" "file://<scratchpad-host>.html[?lang=<code>]"
  • {{HERO_COMPONENT}} resolves to the surface's own hero.component.js when that file exists, and to the shipped assets/hero.js otherwise. That substitution is the whole fork mechanism.
  • --allow-file-access-from-files is required. It lets the host's ES-module imports resolve over file://, which Chrome CORS-blocks otherwise.
  • Use a fresh --user-data-dir per render. A running or stale Chrome locks the profile.
  • Chrome lingers about 60 seconds after writing the screenshot in --headless=new, so the render is backgrounded and polled for the .png rather than blocked on. macOS has no GNU timeout.
  • The stage sits at the page origin, so a W x H window captures exactly the hero, and DPR 2 gives a crisp 2W x 2H source. The component sets document.documentElement.dataset.ready="1" once the font swaps, any logo image decodes, and the title is auto-fit. The default W,H is 1200,630.
  • Blog and docs heroes pass the locale via the URL query (?lang=<code>), one screenshot per locale. A README hero renders once, with no ?lang=.
Was this page helpful?