Generate Shockproof AI branded training presentation modules as a folder of PNG slide images, with optional PDF assembly. Slides are defined as a DeckSpecification JSON, interpreted into HTML/CSS with flexbox, grid, and dynamic font sizing, then rendered to PNG via a cloud function. Use this skill whenever the user asks for a Shockproof AI presentation, training module, or course deck as images or a PDF. Triggers on: "create a module", "build a presentation", "training deck", "Shockproof deck", "SAI slides", "HTML deck", or any request for a branded training presentation. The user specifies the number of slides. The output is a folder of numbered PNG files; a combined PDF can be assembled on request.
This skill generates professional training module PDFs with Shockproof AI branding. You generate a DeckSpecification JSON, which is interpreted by @shockproof/deck-builder into HTML slides, rendered to PNGs via a cloud function, and assembled into PDF.
Components auto-stack in a flexbox column between header and footer. No manual coordinates needed.
| Feature | How It Works |
|---|---|
| Layout model | Flow-based flexbox auto-stacking |
| Text wrapping | Native CSS word-wrap and line-clamp |
| Component placement | Auto-stacked in flex column; vertically centered |
| Multi-column layouts | row component with cardHtml/tableHtml children |
| Card grids (4–8 items) | cardGrid component with Lucide icons per card |
| Rendering | HTML/CSS → cloud function (renderHtmlToPng) → PNG |
| PDF assembly | pdf-lib via @shockproof/deck-builder |
| Variable | Required for | Notes |
|---|---|---|
RENDER_HTML_API_KEY | PNG rendering | API key for the renderHtmlToPng cloud function |
NARAKEET_API_KEY | Video generation | Only needed if generating video |
See SETUP.md for how to generate these values and for local development notes.
references/api_reference.md for the complete JSON schema reference.@shockproof/deck-builder (monorepo: packages/deck-builder/).Text wrapping is fully automatic. CSS handles all word-wrap, line-clamp, and overflow. You never need to manually break strings or calculate positions.
Confirm before generating:
{Series}_Module_{N}_{ShortName}.pdfGenerate a JSON file matching this structure. The full schema is in references/api_reference.md.
Server-side equivalent: The
courseMapDeckFillerChainagent job generates the same DeckSpecification JSON using thedeckSpecGeneratoragent instructions (agents/courseassistant/deckSpecGenerator/instructions.md) and a shared schema reference fragment (agents/courseassistant/shared/deckspec-schema-reference.md). Both paths feed into the same@shockproof/deck-builderpipeline.
{
"config": {
"seriesTitle": "Asset-Based Lending Mastery",
"totalModules": 6
},
"slides": [
{
"type": "title",
"moduleNum": 1,
"title": "Inventory Financing Fundamentals",
"subtitle": "Understanding Collateral Valuation",
"totalPages": 40,
"narration": "Welcome to Module 1 of the Asset-Based Lending Mastery series."
},
{
"type": "content",
"chrome": {
"title": "Learning Objectives",
"moduleNum": 1,
"moduleTitle": "Inventory Financing Fundamentals",
"pageNum": 2,
"totalPages": 40
},
"components": [
{
"type": "row",
"children": [
{ "type": "cardHtml", "accent": "#1A4FE8", "title": "Objective 1", "body": "Identify eligible inventory types" },
{ "type": "cardHtml", "accent": "#2E7D32", "title": "Objective 2", "body": "Calculate advance rates" }
]
},
{
"type": "row",
"children": [
{ "type": "cardHtml", "accent": "#C17D10", "title": "Objective 3", "body": "Evaluate borrowing base" },
{ "type": "cardHtml", "accent": "#B91C1C", "title": "Objective 4", "body": "Spot red flags in aging reports" }
]
}
],
"narration": "In this module you will learn four key objectives."
},
{
"type": "section",
"title": "Core Concepts",
"subtitle": "Building a strong foundation",
"moduleNum": 1,
"moduleTitle": "Inventory Financing Fundamentals",
"pageNum": 3,
"totalPages": 40,
"narration": "Now lets dive into core concepts."
},
{
"type": "content",
"chrome": { "title": "Process Overview", "moduleNum": 1, "moduleTitle": "Inventory Financing Fundamentals", "pageNum": 4, "totalPages": 40 },
"components": [
{ "type": "stepRow", "num": 1, "title": "First Step", "description": "Description of step one." },
{ "type": "stepRow", "num": 2, "title": "Second Step", "description": "Description of step two." },
{ "type": "stepRow", "num": 3, "title": "Third Step", "description": "Description of step three." },
{ "type": "calloutBox", "title": "Key Insight", "body": "Summary of the process." }
],
"narration": "This slide outlines the three-step process."
},
{
"type": "keyTakeaways",
"moduleNum": 1,
"moduleTitle": "Inventory Financing Fundamentals",
"pageNum": 38,
"totalPages": 40,
"takeaways": [
{ "title": "Takeaway 1", "desc": "Description..." },
{ "title": "Takeaway 2", "desc": "Description..." },
{ "title": "Takeaway 3", "desc": "Description..." },
{ "title": "Takeaway 4", "desc": "Description..." }
],
"narration": "Lets review the four key takeaways from this module."
},
{
"type": "references",
"moduleNum": 1,
"moduleTitle": "Inventory Financing Fundamentals",
"pageNum": 39,
"totalPages": 40,
"references": [
{ "category": "Regulatory Guidance", "items": ["OCC Handbook", "FDIC Manual"] }
],
"narration": "Here are the key references for further reading."
},
{
"type": "closing",
"moduleNum": 1,
"moduleTitle": "Inventory Financing Fundamentals",
"nextModuleNum": 2,
"nextModuleTitle": "Accounts Receivable Analysis",
"totalPages": 40,
"narration": "Thank you for completing Module 1.\n\n(pause: 1)\n\nThank you."
}
]
}
Before writing any component, ask: can each item be given a 2–5 word title/headline and a description sentence? If yes, use cardGrid (4–8 items) or a row of cardHtml (2–3 items). This is the default visual treatment for enumerable content.
cardGrid if each can be namedbullets when items genuinely share no individual title (e.g. a list of sub-points elaborating a single idea)redFlagPairs for pure short warning phrases that cannot be given distinct namesRead references/api_reference.md for the full JSON schema. All slide types, component types, and options are documented there.
Content is vertically centered. Components stack at their natural height between header and footer. The flex layout vertically centers them, producing balanced whitespace above and below. Components must NOT use flex:1 in the column direction — that would stretch them to fill the area and defeat centering.
Page numbers must be sequential. Track pageNum for every slide. The title slide is page 1.
section and closing slides do not use chrome. They have their own special layouts. Only content slides have a chrome object.
keyTakeaways creates chrome internally. Do not wrap it in a content slide. It is its own slide type.
references creates its own slide. Do not wrap it in a content slide.
redFlagPairs goes inside a content slide. It does NOT create chrome — the parent content slide provides chrome.
Bold-prefix format. Use "Key Term: rest of description" — the colon triggers auto bold formatting.
rowH in styledTable opts is in INCHES, not pixels. It is multiplied by SCALE=128 internally. rowH is the primary lever for controlling whitespace within table rows — increase it for spacious rows, decrease for compact rows. cellPadding is secondary and only effective when rowH is large enough to accommodate it.
0.35 in ≈ 45px — baseline for most tables0.45–0.55 in — use when few rows leave visible empty space below0.28 in ≈ 36px0.22 in ≈ 28px"rowH": 36 — that would be 36 × 128 = 4,608px per row.rowH above the default to fill available space.stepRow height budget. Each step row is ~55px. Budget for the content area (~560px after chrome):
calloutBox (~80px): ~373px OKstyledTable instead.cardHtml body accepts arrays for bullet lists. Pass an array of strings to render as bulleted list:
{ "type": "cardHtml", "accent": "#1A4FE8", "title": "Topic", "body": ["Item one", "Item two", "Item three"] }
Use hex color strings. Always use full hex like "#1A4FE8", not color names or shorthand.
Every slide should have a narration string field. Narrations are read directly from the DeckSpecification JSON by the build pipeline — no separate narration file is needed.
$17719 not $17,719)negative 204000 dollarscashflow (one word, always)(pause: N) with \n\n before and after — never inline within a sentence. Example: "Final thought.\n\n(pause: 1)\n\nThank you."\n\n(pause: 1)\n\n before the thank-youBefore writing the JSON, scan every narration string for:
& → reformatcash flow (two words) → replace with cashflow(pause: N) directives inline → move to \n\n(pause: N)\n\n format\n\n(pause: 1)\n\nThank you.When narration is present, the build pipeline auto-generates:
narakeet-script.md — Narakeet video script pairing PNGs with narrationnarakeet.zip — self-contained archive for Narakeet API uploadVideo generation uses the Narakeet API and requires NARAKEET_API_KEY.
After the deck build completes (PNGs + narakeet.zip exist in the output directory), use the shared renderer's submitToNarakeet() method to upload the zip, poll for completion, and download the MP4:
const path = require('path');
const RENDERER_ROOT = path.join(__dirname, '../../shared/html-slide-renderer');
const tpl = require(path.join(RENDERER_ROOT, 'scripts/sai_html_template.js'))({
seriesTitle: 'Your Series Title', totalModules: 1,
});
const pres = tpl.createPresentation();
const videoPath = await pres.submitToNarakeet(outputDir, {
videoFilename: 'My_Module.mp4',
});
console.log(`Video saved: ${videoPath}`);
How it works internally:
NARAKEET_API_KEY from env (falls back to gcloud secrets CLI)https://api.narakeet.com/video/upload-request/zipnarakeet.zip to the pre-signed URLPOST /video/buildoutputDir/{videoFilename}Key file: .claude/skills/shockproof-skills/shared/html-slide-renderer/scripts/presentation.js — the submitToNarakeet() method (line ~218).
After generating the DeckSpecification JSON, run it through @shockproof/deck-builder:
import { buildDeck } from '@shockproof/deck-builder';
import fs from 'fs';
const spec = JSON.parse(fs.readFileSync('deck-spec.json', 'utf8'));
const result = await buildDeck(spec, {
type: 'filesystem',
outDir: './mnt/outputs/Module_1',
}, {
pdfName: 'ABL_Mastery_Module_1.pdf',
});
console.log(`Built ${result.slideCount} slides`);
console.log(`PDF: ${result.pdfPath}`);
The same JSON can also be submitted as a cloud agent job (htmlDeckBuilder) for server-side execution.
| Content Type | Best Component |
|---|---|
| 4–8 items that each have a title/headline AND a description (any topic — examples, warnings, concepts, risks, case studies) | cardGrid |
| Introduction / overview (2–3 topics) | row with 2–3 cardHtml children |
| Numbered process / sequential steps | stepRow (max 5 alone, or 4 with callout; use styledTable if more) |
| Bulleted knowledge points with NO natural title | bullets (max 6–7 items) |
| Checklist / requirements | checklist (max 8 items) |
| Data / metrics comparison | styledTable |
| Key statistics | row with 3 cardHtml (statCard style) |
| Contrasting approaches (do/don't) | comparison |
| Important tip or principle | calloutBox |
| Warning signs / red flags — pure short phrases, no titles | redFlagPairs (exactly 6 pairs) |
| Two-topic deep dive | row with 2 cardHtml + calloutBox |
cardGrid whenever items have titlesIf the content has 4–8 items and each item has a natural headline or lead phrase, always use cardGrid — even if the items are warnings, risks, case studies, or red flags. The rule of thumb: if you could write a 2–5 word title for each item, it should be a card.
bullets is only correct when items have no natural title — they are sentence-length facts or sub-points that don't decompose into a headline + body.
redFlagPairs is only correct when items are pure, short, equal-weight warning phrases that can't be given distinct titles. If you can name them (e.g. "Lehman Brothers", "Washington Mutual"), they are cards, not flag pairs.
cardGridbulletsstyledTablestepRowAvoid repeating the same component layout on consecutive slides. Use at least 8 different component types across a module.
The build pipeline includes an automated visualCheck phase that detects content overflow and text truncation, then auto-fixes affected slides.
renderHtmlToPng cloud function evaluates overflow-detection JS on each slide after DOM layout, before taking the screenshotoverflow metadata alongside the PNGs:
contentClipped: boolean — .slide-content children exceed the visible areatruncatedElements: string[] — specific elements with scrollHeight > clientHeight (text cut off by overflow: hidden)titleTruncated: boolean — the title element's scrollHeight > clientHeight (title text cut off by -webkit-line-clamp)buildDeck reads the overflow metadata and auto-applies fixes to the DeckSpecification:
compact: true (14pt titles, 11pt descriptions, tighter padding)compact: truefontSize by 2pt (min 9pt)compact: truerowH by 0.07in (min 0.22in) first; only compacts cellPadding (8px 10px → 6px 8px → 4px 6px) when rowH is already at minimumrowH by 0.05in (max 0.55in) first; only expands cellPadding (8px 10px → 10px 14px → 12px 16px) when rowH is already at maximumtitleFontSize is reduced to fit on one line (minimum 18pt). 2+ words on the second line is acceptable and does not trigger downsizing. The fix applies at most once per slide — if a titleFontSize override already exists, no further downsizing occurs. If a previous downsize caused title truncation (detected via titleTruncated in the overflow metadata), the override is reverted to restore the natural title size.result.overflowWarnings// Enabled by default
await buildDeck(spec, outputConfig);
// Disable if needed
await buildDeck(spec, outputConfig, { visualCheck: false });
visualCheck resultsoverflowWarnings are reported in the build result, those slides need manual attentionnarration field