Packages finished enablement material into a multi-format HTML handoff. Always writes one offline .html file and can also emit a generic Apps Script web app bundle. Use for distribution, not drafting.
How this skill is triggered — by the user, by Claude, or both
Slash command
/enablement-html-renderer:enablement-html-rendererThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
A handoff target, not a starting point. Other drafting workflows produce the
A handoff target, not a starting point. Other drafting workflows produce the substance; this skill packages it into a single HTML file a person opens anywhere and reads in the format that suits how they absorb information.
The core idea: people absorb the same knowledge differently. Some want terse
bullets, some want prose, some want a diagram, some want it as a comic. Instead of
producing four artifacts, this produces one file with a format selector built in.
All four representations of the same content ship inside the file. No server, no
build step, no dependency: it is one .html you email, drop in a channel, or
attach to a meeting summary.
.docx/.md).A calling skill passes a content bundle: a small JSON or markdown object with the source material already written. This skill never invents the substance; it only transforms and packages what it receives. Minimum fields:
{
"title": "Claude Code gotchas, week of 16 Jun",
"kind": "training | meeting | workshop | guide",
"core_idea": "The one thing the reader must leave with.",
"sections": [
{
"heading": "...",
"body": "the prose source of truth for this section",
"estMinutes": 5,
"videoUrl": "https://loom.com/share/...?t=52",
"callouts": [
{ "level": "critical | warn | tip | never", "text": "the one thing not to get wrong" }
],
"commands": ["colima stop", "colima start"],
"images": [
{ "src": "data:image/png;base64,iVBORw0KGgo...", "alt": "the login screen", "caption": "Where the SSO button lives" }
]
}
],
"agenda": ["optional list of items covered"],
"tasks": [{ "who": "[NAME]", "what": "action", "due": "date or blank" }],
"links": [{ "label": "Loom walkthrough", "url": "..." }]
}
sections[].body is the source of truth. The four prose formats are all
derived from it during the Shape phase, so they never contradict each other.
Optional per-section fields make the artifact match how real enablement material
reads: estMinutes shows a time badge, videoUrl adds a deep-link to the exact
moment in a recording, callouts carry severity (critical/warn/tip/never) that
renders consistently across every format, and commands are copy-paste-ready code
lines. agenda, tasks, and links are optional and render as their own panels.
Recordings (Loom, Zoom) belong in links or per-section videoUrl so the file
complements video rather than replacing it.
Images (real screenshots or supplied art). Images are the big lever on content quality - a
real screenshot of the actual screen next to the words that explain it beats any amount of text,
and many callers already have images to hand (a published blog with inline shots, a workshop deck,
a product UI capture). There is no cap on how many and no fixed slot: put as many images as
the content needs, wherever they belong. You decide placement from what you are asked to illustrate.
Each image is { src, alt, caption }. Three ways to place them, mix freely:
section.images: [ {src,alt,caption}, ... ]. A convenience for "here
are the shots for this section". Renders together near the top of the section. Any number.bullets entry can be a string (text) or an object: {text} is a
text bullet, {img} (or {image}) is a standalone image, {text, img} is a bullet followed by
its image. Order is preserved, so an image can sit before, after or between any bullets - e.g. one
screenshot per point.prose is authored HTML, so drop an <img src="..."> (or a <figure> with
a <figcaption>) anywhere in the paragraph flow, including between two sentences. If the prose is
five or six sentences, that can be one image per sentence. The renderer styles .prose img.src takes an http(s) URL (already-online content, kept as-is) or a data:image/*;base64 URI
(local files, inlined so the file stays one offline artifact). Run scripts/img_to_datauri.py --json shot.png ... to convert local images into paste-ready entries. src is sanitised at render time to
data:image/* or http(s) only; alt/caption are escaped plain text (in bullets/section images).
Keep embedded weight sane: base64 inflates by ~34%, so many multi-MB images in one file adds up -
size them for the web first if you are inlining a lot. During Shape, look for images the caller
already has before drawing a visualSvg from scratch; a genuine screenshot usually beats a diagram,
and the two can coexist (screenshot for the real thing, SVG for the abstract flow).
Trusted vs escaped fields. Two fields are authored by this skill and pass
through as HTML: prose (you write the paragraph markup) and each section's
visualSvg (you write the inline SVG). Everything else (headings, bullets, comic
scene/say text, agenda items, task text, link labels) is treated as untrusted
plain text and HTML-escaped at render time, so a code snippet like Skill<X> or an
ampersand shows literally instead of breaking the page or injecting markup. Link
URLs are sanitised to http/https/mailto only. Put code and angle brackets in the
escaped fields freely; reserve prose/visualSvg for markup you intend.
Auto-linking (URLs only in the shared build). At render time the page turns any http(s) URL
into a real link. This runs across every plain-text field (headings, bullets, comic text,
callouts, captions, agenda, tasks) and inside prose HTML - in prose it only touches text nodes,
never re-escaping and never re-linking content already inside an <a>. Net effect: never paste a
dead URL; write the full URL in any field and it resolves itself. Link-panel labels are left
un-linkified to avoid nested anchors (the row already links to its url), and code in commands
is never linkified. Issue-key auto-linking is intentionally not the default in the shared build,
because a shared plugin should not assume one tracker.
Theming visualSvg (so diagrams survive dark mode). Do not hardcode near-black
or brand colours for diagram strokes and text; they vanish on a dark background.
Use currentColor for lines and labels (it inherits the reader's theme ink and
flips automatically) and var(--accent) / var(--accent2) for highlights. The
renderer runs a normalizeSvg pass that remaps the legacy palette
(#1a1a1a to currentColor, #2f5d50 to var(--accent), #c4622d to
var(--accent2)) so older diagrams flip too, but author new SVGs with
currentColor from the start rather than relying on the safety net. Leave
fill='none' alone and keep any genuinely light fills explicit.
Use the following working order and self-check each step before you move on.
Intake - read the content bundle (or the upstream skill's working doc) in full. You should be able to restate the title, kind, and core idea in one line each.
Shape - for each section, derive all four prose representations from the
same body:
mood of
calm/stressed/alarmed/relieved and an optional prop of coffee/warning/check).
Speech bubbles, numbered tabs, comic linework; no external image dependency.
The fifth format, Cheat sheet, is assembled automatically from the section
headings, the highest-severity callouts, and the commands arrays into one
copy-all reference card, mirroring how your real guides end with a copyable
quick-reference block. Every section should have all four prose forms, or an
explicit visible fallback note for any format that genuinely does not fit, they
should agree on the facts, and any callouts/commands should be attached.Voice gate (AI-isms). Enablement docs are often built from a published blog,
so the displayed prose must stay in that voice, not drift into generic AI
phrasing. Treat body as source input, not something to rewrite in place.
Before rendering, scrub the derived bullets/prose of AI-isms and corporate filler:
no "in today's rapidly evolving", "it's worth noting that", "let's dive in", "at
the end of the day", "delve", "revolutionary", "game-changing", "seamless",
"leverage", marketing fluff, false expertise, or excessive hedging. This mirrors
the AI-ism removal pass in a content-cleanup workflow. Run
scripts/check_ai_isms.py
on the rendered file as part of Validate.
Render - emit one self-contained HTML file from the template, and optionally a generic Apps Script web app bundle from the same rendered source. The offline file should open standalone (no network needed), the selector should switch formats, it should print cleanly, and the hosted bundle should be structurally ready to deploy.
Validate - run the hard gates (below) and self-check the file.
Complete - present the offline file, the hosted bundle if requested, and any
live /exec URL only when you actually deployed it.
A single sticky control at the top: Bullets / Prose / Visual / Comic / Cheat
sheet. Picking one re-renders every section in that format in place. Defaults to
Bullets. The
offline file reflects the choice in the URL hash (for example recap.html#prose).
The hosted Apps Script path should honour top-level #prose and ?fmt=prose
links by using the Apps Script browser APIs (google.script.url and
google.script.history) rather than trusting the iframe's own window.location.
No localStorage or sessionStorage, which break in sandboxed viewers. A
"Print / Save as PDF" button prints whatever format is showing. A small "Show all
formats" option stacks them for people who want to compare.
Accessibility floor: semantic headings, keyboard-operable selector, prefers-color scheme respected, body text at least 16px, diagrams have text captions so the visual format is not the only carrier of meaning.
All templates/... and scripts/... paths below are relative to this skill
directory. In a downloaded standalone zip, that means the folder containing
SKILL.md. In a plugin checkout, resolve them from
plugins/enablement-html-renderer/skills/enablement-html-renderer/.
Read templates/renderer-template.html
and inject the shaped content as a JS data object. The template already contains the selector logic, the four render
functions, print styling, and the panels for agenda/tasks/links. Keep everything
inline (CSS in <style>, JS in <script>); the offline output must remain a
single file.
In Cowork-style sandboxes, write the offline fallback to
/mnt/user-data/outputs/<slug>.html. Outside that environment, write to the
caller's requested output directory or another explicit local path and report the
exact location.
When the caller wants the hosted path too, also write a bundle directory at
/mnt/user-data/outputs/<slug>-gas/ in Cowork-style sandboxes, or the matching
local output directory elsewhere, containing:
Index.html - the same rendered HTML artifact, adapted for HtmlService hosting.Code.gs - copied from templates/gas/Code.gs.appsscript.json - copied from templates/gas/appsscript.json. Default access is ANYONE for public sharing; override to DOMAIN if deploying within a Google Workspace.gas-deploy.sh - copied from scripts/gas-deploy.sh so the bundle can be
deployed in place.Access level gate (required before writing appsscript.json). When the caller requests a hosted bundle, always ask which audience should access the deployed web app before writing appsscript.json:
AskUserQuestion:
header: "Who should be able to open the hosted web app?"
options:
A) Anyone with the link (ANYONE) - recommended for public sharing
description: "Any Google account (or no account) can open the /exec URL. Works outside any domain."
B) My Google Workspace domain (DOMAIN) - for internal team use only
description: "Anyone in your Google Workspace organisation can open the /exec URL."
If the user skips or dismisses without answering, stop and surface an error:
"webapp.access must be set before generating the bundle. Please choose ANYONE or DOMAIN."
Never silently default - the choice must be explicit.
Write the chosen value into appsscript.json before copying it into the bundle.
If GAS_WEBAPP_ACCESS is already set in the environment, use that value and skip the question (non-interactive context).
Note for Google Workspace users: if your domain administrator has disabled ANYONE access, clasp deploy will print "ANYONE access has been disabled" even if the deployment succeeded. Check clasp deployments for the actual result - the deployment ID and URL will be present if it succeeded.
Do not add Toast, AWS, region, CMA, agent, secret, or external-request logic. The bundle is only the static-hosting layer around the same renderer.
Claude Code deploy path. If clasp is available and authenticated, you may run
the bundled deploy helper or the equivalent explicit clasp commands for real.
Return a live /exec URL only after the deploy actually succeeded.
Cowork sandbox path. Always write the bundle, plus print the exact clasp
commands the user should run later. Never imply a live deployment exists when you
did not perform one.
If a format genuinely cannot be derived for a section (for example, a legal disclaimer has no sensible comic), render that section in its best format and add a visible note rather than faking a panel. Never claim a format that is not really there.
Validation limits (be honest about these). Headless DOM checks (jsdom) verify structure, escaping, link sanitising, format switching, and the copy handler, but they do not compute layout or rendered colour, so on their own they cannot catch viewport problems or a diagram that renders dark-on-dark (the exact bug that shipped twice). Two mitigations, neither a full substitute for a real open:
scripts/rasterize_diagrams.py on the rendered file (a hard gate
below). It resolves every Visual diagram in both light and dark themes (the same
currentColor / accent-var substitution the browser does) and fails when a diagram's
ink would not contrast with the theme background, so a dark-on-dark regression becomes
a loud failure instead of a silent one. The check is pure-Python with no third-party
dependency, so it runs identically in Cowork and Claude Code.Whenever the control strip or the diagram theming changes, treat a real on-device open (phone width, dark mode toggled) as the validating signal before raising confidence. The diagram-contrast gate protects against colour regressions between those opens; it does not replace them.
For the hosted Apps Script path, do one real deploy before shipping the feature and
open the /exec URL. Specifically confirm whether the format selector still works
through direct links and whether the Print button behaves usefully inside
HtmlService. In the validated path, direct links work when the hosted page reads the
outer /exec URL through the Apps Script browser APIs, and the Print button still
calls window.print() from inside the iframe. If a tenant policy blocks broader
access levels like ANYONE or ANYONE_ANONYMOUS, keep the default DOMAIN flow or
fall back to MYSELF and say so plainly.
Run these from the skill directory, or resolve the same relative paths from the skill root explicitly.
python3 scripts/measure_frontmatter_weight.py SKILL.md
bash scripts/check_em_dash.sh SKILL.md
python3 scripts/pii_scan.py SKILL.md
python3 scripts/check_ai_isms.py <rendered-output.html>
python3 scripts/rasterize_diagrams.py <rendered-output.html>
The scripts are vendored into this plugin's own scripts/ folder, so the skill
validates self-contained without depending on the skills-toolkit plugin.
rasterize_diagrams.py is dependency-free: it computes light/dark contrast in pure Python
and fails on a hardcoded near-greyscale colour that would not flip with the theme. It also
writes one preview per diagram per theme for eyeballing - a PNG if cairosvg happens to be
installed, otherwise a standalone SVG - but the pass/fail never depends on that optional
library, so it runs the same in Cowork and Claude Code.
Plus the two non-script gates: name-collision (this name is not a single verb and does not collide) and artefact-type triage (this is a skill that runs on demand, not a hook that fires on an event). Tasks carrying real attendee names must use placeholders in any committed example; real names only in the generated output the user keeps.
Do not claim the hosted path is live unless you actually ran the deploy and opened
the resulting /exec URL. If clasp is unavailable, unauthenticated, or blocked,
say so plainly and leave the user with the offline file plus the exact bundle path
and commands.
Guides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Synthesizes the current conversation into a structured spec (PRD) and publishes it to the project issue tracker with a ready-for-agent label, without interviewing the user.
npx claudepluginhub youngleadersdottech/young-leaders-tech-marketplace --plugin enablement-html-renderer