FAQ
Short, practical answers to the questions that come up most — embedding live content, writing richer doc comments, and a few common configuration tweaks. Each recipe links to the page with the full details.
Embedding live content
The theme can drop a sandboxed <iframe> into any page — a CodePen, a YouTube video, a StackBlitz, or any site. There are two ways to author one, and they share one config grammar:
- In prose (README, guides, the
docsfolder) — an```iframefenced block. - In a doc comment — the
@iframe <url> key=valueblock tag.
The first token is the URL; the rest are key=value options — title, height (px), aspectRatio (e.g. 16/9), width, allow, sandbox, clickToLoad, and themed. Only https:// (or protocol-relative //) URLs are accepted.
How do I embed a CodePen?
Use the pen's embed URL (https://codepen.io/USER/embed/PEN_ID):
```iframe
https://codepen.io/USER/embed/PEN_ID title="CodePen demo" height=400
```On CodePen, open Embed and copy the URL from the
<iframe src="…">snippet. AddclickToLoad=trueto show a light poster until the reader clicks.
How do I let readers open an example in CodePen / JSFiddle / CodeSandbox?
@iframe embeds an existing pen by URL. To open the code from an @example — prefilled, no pen needed — use the playground feature instead: turn it on in opts.playground, then tag the example with @playground:
/**
* @example
* const out = resize(img, 200);
* @playground codepen jsfiddle filename=resize.js highlight=1
*/
export function resize(img, width) {}The code block's header gains an "Open Code in" dropdown (CodePen / JSFiddle / CodeSandbox), all client-side — no API key. It also works in prose (a ```js playground fence or a <playground> block). Full walkthrough: Add a playground. (@playground needs tags.allowUnknownTags: true, same as @iframe.)
How do I embed a YouTube video?
Use the embed URL (https://www.youtube.com/embed/VIDEO_ID) and give it a 16/9 aspect ratio instead of a fixed height:
```iframe
https://www.youtube.com/embed/VIDEO_ID title="Intro video" aspectRatio=16/9
```How do I embed any other website?
Any https:// URL works — set a height in pixels or an aspectRatio:
```iframe
https://example.com title="Live preview" height=480
```How do I embed from a JSDoc / TypeDoc comment?
Use the @iframe block tag — it renders after the symbol's @example:
/**
* Renders the chart.
*
* @iframe https://codepen.io/USER/embed/PEN_ID title="Demo" height=400
*/
export function render() {}
@iframeis an unknown tag to base JSDoc — settags.allowUnknownTags: truein yourjsdoc.json, or JSDoc strips it before the theme runs. (TypeDoc needs no such flag.)
My embed isn't showing — what's wrong?
- The URL must start with
https://(or//). Plainhttp://or a relative path is rejected, and the embed is silently dropped. - Unknown option keys are ignored with a build warning — check spelling against the list above.
Can I make it load on click instead of immediately?
Add clickToLoad=true: the reader sees a poster button (with a <noscript> fallback) and the iframe loads on click. By default it loads immediately and works with no JavaScript at all.
Do embeds follow the light / dark theme?
By default, yes — the embed URL is re-resolved when the theme changes (a {theme} token is swapped, or ?theme-id=<theme> is appended). Opt out with themed=false. Full reference: Embeds & live demos.
Richer doc comments
Everything the theme does in prose also works inside your JSDoc / TypeDoc descriptions — they flow through the same converter.
How do I add a callout in a comment?
Write a GitHub-style alert blockquote right in the description:
/**
* Connects to the database.
*
* > [!WARNING]
* > Call `close()` when you're done — connections are not pooled.
*/
export function connect() {}The markers map to four styles: [!NOTE] / [!INFO] / [!IMPORTANT] → info, [!TIP] / [!SUCCESS] → tip, [!WARNING] / [!CAUTION] → warning, and [!ERROR] / [!DANGER] → error. See Callouts.
Can I use steps or tabs in a comment?
Yes — the same <steps> (and <tabs>) markup works in a description; there's no dedicated tag, you write the markup directly:
/**
* @module my-api
*
* <steps>
*
* <step label="Install">
*
* ```sh
* npm install my-api
* ```
*
* </step>
*
* <step label="Use">
*
* ```js
* import \{ go \} from 'my-api';
* ```
*
* </step>
*
* </steps>
*/See Steps and Tabs for the full syntax and the blank-line rule, and the live sample-api module page for it rendered.
What about deprecation notices?
Just use @deprecated — the theme renders it as a callout automatically, no marker needed:
/**
* @deprecated Use `@link connect` instead.
*/Configuration tweaks
How do I use a logo instead of the site-name text?
Set siteName to a logo object with light / dark image paths (and an alt fallback) — see siteName.
How do I control the sidebar grouping and order?
Use @category / @order on symbols, frontmatter group / order on guide pages, and the sectionOrder option. Structure your sidebar covers every lever.
Those levers control the JSDoc API sidebar. For TypeDoc, the API sidebar is a module/folder hierarchy — only doc groups and
menurespond to the options above. See TypeDoc flavor.
My @category / @order / @playground / @iframe tags aren't working
The most likely cause: tags.allowUnknownTags isn't true in your jsdoc.json. These are all tags base JSDoc doesn't define, so it strips them before the theme runs — your categories collapse to the default kind sections, @order does nothing, and @playground / @iframe never render. Set the flag:
{ "tags": { "allowUnknownTags": true } }(TypeDoc has no such restriction — it passes these through.) See Custom tags for the full list.
How do I add hand-written guides next to the API reference?
Point opts.docs at a folder of Markdown. See Build a guides site and Combine guides + API.
How do I add a custom footer?
Set footer to a string of HTML — it renders at the bottom of every page, in place of the default footer:
opts: {
template: "./node_modules/clean-jsdoc-theme",
footer: "<div class='site-footer'>© 2026 My Library — built with care</div>"
}For anything longer than a line, hand it a file instead — it's read at build time and keeps your config tidy:
opts: { footer: { file: "./footer.html" } }Style it with your own CSS. The footer carries whatever classes you put in your markup; style them with customCss / customCssFile, which the theme loads after its own stylesheet, so your rules win without !important:
opts: { footer: { file: "./footer.html" }, customCssFile: "./footer.css" }/* footer.css */
.site-footer { padding: 2rem 0; text-align: center; color: var(--clean-text-muted); }
.site-footer a { color: var(--clean-primary); }Need behaviour (e.g. a dynamic year)? Add customJs / customJsFile — it runs last, after the theme's own scripts. Most footers don't need it.
How do I add custom meta tags (SEO, Open Graph, Twitter cards)?
Set meta to an array of attribute objects. Each object becomes one <meta> tag in every page's <head> — the keys are the attributes:
opts: {
meta: [
{ name: "description", content: "Fast, typed docs for My Library" },
{ name: "keywords", content: "jsdoc, typescript, documentation" },
{ name: "theme-color", content: "#0b0b0b" }
]
}Open Graph and Twitter cards work the same way — they're just <meta> tags:
opts: {
meta: [
{ property: "og:title", content: "My Library" },
{ property: "og:image", content: "https://example.com/social-card.png" },
{ name: "twitter:card", content: "summary_large_image" }
]
}A few things worth knowing:
charsetandviewportare already emitted — you don't need to add them.- A
generatortag is added —<meta name="generator" content="clean-jsdoc-theme <version>">records which theme version built the site. Pass your own{ name: "generator", … }to replace it. - Your tag wins over the theme's default. A
{ name: "description", … }replaces the auto description; you won't get two. - These are site-wide — the same tags render on every page (per-page social cards aren't supported yet).
- Values are escaped automatically, so quotes and angle brackets are safe.
How do I set a favicon?
Point favicon at an image file. The theme copies it to a content-hashed asset and adds a <link rel="icon"> to every page's <head>:
opts: { favicon: "./assets/logo.svg" }This is the way to use an SVG favicon — browsers only auto-discover a root favicon.ico, never an SVG, so it needs the <link> the theme emits. (An SVG can even adapt to light/dark via a @media (prefers-color-scheme: dark) block inside it.) The v4 favicon option was briefly dropped early in v5 and is back.
How do I turn off the "copy page" / "open in LLM" button?
Configure or disable it with copyPage.
Why does my build warn about an unknown option?
Unknown or misspelled options warn by default (with a "did you mean?" hint) and the build continues. Set strict to turn those warnings into errors.
My dist is empty and every page fails with "Cannot read properties of undefined (reading 'context')"
Upgrade to 5.0.8 or newer. Earlier versions declared preact as a plain dependency in the theme's internal packages, so under Yarn Berry / Plug'n'Play the server renderer and the components could bind to two different Preact instances — which made every page fail to render and left dist empty. 5.0.8 declares preact as a peer dependency so a single instance is always shared.
If you can't upgrade yet, switch that project off PnP by adding to .yarnrc.yml:
nodeLinker: node-modulesthen re-run yarn install.
Localization
How do I build a localized (multi-language) site?
Declare your languages in the same opts block and drive the build with the clean-jsdoc CLI (the @clean-jsdoc-theme/aadesh package):
{
"opts": {
"locales": [
{ "code": "en", "name": "English" },
{ "code": "ja", "name": "日本語" }
],
"defaultLocale": "en"
}
}npm i -D @clean-jsdoc-theme/aadesh
clean-jsdoc i18n extract # build the per-locale translation catalogs
# …translate the JSON (or `clean-jsdoc i18n prompt` for an LLM prompt)…
clean-jsdoc build # render one static site per localeYou get one site per language (the default at the root, others under /<locale>), a header language switcher, and hreflang tags. Prose is localized by file — a README.<locale>.md home page and a docs.<locale>/ overlay — and fonts per locale via "ja:heading"-style keys. The full walkthrough is in Localize your docs.
I used to run clean-jsdoc extract — where did it go?
The localization commands now live under an i18n group, so the CLI has room to grow beyond localization. Update your commands (and any package.json scripts):
| Old | New |
|---|---|
clean-jsdoc extract | clean-jsdoc i18n extract |
clean-jsdoc prompt | clean-jsdoc i18n prompt |
clean-jsdoc validate | clean-jsdoc i18n validate |
clean-jsdoc build | clean-jsdoc build (unchanged) |
build stays top-level because it renders your site whether or not you use multiple locales — the per-locale fan-out is just what build does when opts.locales is set. All flags are unchanged. Run clean-jsdoc with no arguments for the interactive menu, which now groups the i18n steps together.
Working with LLMs
I use an LLM for development — how do I get the most out of clean-jsdoc-theme?
clean-jsdoc-theme is built to be LLM-friendly, in two directions:
- Your generated site is readable by AI. Every page emits a companion Markdown file (
<page>/index.md), and a per-page copy / open-in-LLM button hands the clean.mdstraight to Claude / ChatGPT / Perplexity — so an assistant reads the exact same reference your users do. Tune the handoff withcopyPageandaiPrompt. - Setting up the theme is itself AI-assisted. There's a downloadable skill that turns any coding assistant into a clean-jsdoc-theme expert — point your agent at it and it can write your
jsdoc.json/typedoc.json, author guides with callouts/steps/tabs, structure the sidebar, wire up cross-links, set up localization, and debug a build, all from source-verified knowledge instead of guesswork. Drop the skill folder into your assistant (e.g..claude/skills/) and just ask for what you want.
So whether the LLM is reading your docs or building them, you don't have to do the translating — point it at the companion .md and the skill.