SuperAPI AI Directory

iblai-api-studio-html

Frontend & websites

View repository

Updated 10-16 05:56

About this skill

iblai-api-studio-html

The html component is the workhorse of course content: any HTML the user should read. Create it, then write data (the HTML) and metadata.display_name (the title). Two calls per component; no editor round-trips.

Before you start

  1. Preflight: node .claude/skills/iblai-api-studio-auth/scripts/studio-login.mjs --check prints two oks; otherwise run /iblai-api-studio-auth first — do not attempt the calls below.
  2. A unit locator (…+type@vertical+block@…) from /iblai-api-studio-unit or /iblai-api-studio-outline.
  3. If images or files are referenced, upload them first (POST /assets/{course}/, /iblai-api-studio-settings) and use their /static/… paths.

Auth & conventions

  • Base URL: $STUDIO_URL; Studio session cookies from studio.env (its values are single-quoted — bash strips the quotes; any other parser must strip them too, or Studio answers 500: /iblai-api-studio-auth).
  • Snippet:
    set -a; . ./studio.env; set +a
    S=(-s -b "studio_session_id=$STUDIO_SESSION; csrftoken=$STUDIO_CSRF" -H "X-CSRFToken: $STUDIO_CSRF" \
       -H "Origin: $STUDIO_URL" -H "Referer: $STUDIO_URL/" -H "Accept: application/json" -H "Content-Type: application/json")
    
  • Keys: parent = a unit locator; the block is …+type@html+block@<hex>.
  • Content is a draft until the unit is published (/iblai-api-studio-publish). DELETE is destructive — confirm with the user first.
  • A 500 HTML page on these calls = malformed session cookie (/iblai-api-studio-auth, "Reading failures"), not bad HTML — Studio stores any string.

Reads

  • GET /xblock/{html} — { "id", "display_name", "category": "html", "data": "<html string>", "metadata": { "display_name" }, "published", "has_changes", … }.
  • GET /api/v1/ibl/xblock/get-details?locator=<url-encoded html locator> — { "id", "data", "display_name" } (lighter; 406 for non-html blocks).
  • GET /api/contentstore/v1/container/vertical/{unit}/children — the unit's components in order (block_type: "html", name, block_id).

Writes

  • POST /xblock/ — create an empty text block (appends to the unit):
    { "parent_locator": "<unit>", "category": "html" }
    
    → { "locator": "…+type@html+block@<hex>", "courseKey": "…" }. Optional "boilerplate": "announcement.yaml" pre-fills an announcement template; "display_name" is ignored on create — set it in the update.
  • POST /xblock/{html} — write the content (this is the whole edit):
    { "data": "<section>…</section>", "metadata": { "display_name": "Why data matters" } }
    
    → { "id", "data": "<the stored HTML>", "metadata": { "display_name": "…" } }. data replaces the previous body entirely; metadata is merged. (Equivalent v1 route: POST /api/v1/ibl/xblock/update { "locator", "category": "html", "courseKey", "data", "metadata": { "display_name" } } → 200; /iblai-api-studio-outline.)
  • POST /xblock/ { "duplicate_source_locator": "<html>", "parent_locator": "<unit>" } — copy into the same or another unit.
  • DELETE /xblock/{html} → 204. Confirm with the user first.

Example

set -a; . ./studio.env; set +a
S=(-s -b "studio_session_id=$STUDIO_SESSION; csrftoken=$STUDIO_CSRF" -H "X-CSRFToken: $STUDIO_CSRF" \
   -H "Origin: $STUDIO_URL" -H "Referer: $STUDIO_URL/" -H "Accept: application/json" -H "Content-Type: application/json")
UNIT="block-v1:acme+DATA101+2026-T1+type@vertical+block@645f5ded5f544adab9fb603c4afc3bac"

HTML=$(curl "${S[@]}" -X POST "$STUDIO_URL/xblock/" -d "{\"parent_locator\":\"$UNIT\",\"category\":\"html\"}" \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["locator"])')

# Build the body in a file so quotes and newlines survive; send it as JSON.
cat > body.html <<'HTML'
<section>
  <h3>Why data matters</h3>
  <p>Every decision in this course starts with a question and ends with evidence.</p>
  <div style="border-left:4px solid #0058cc;background:#f3f7ff;padding:12px 16px;margin:16px 0;border-radius:6px;">
    <strong>Key idea.</strong> A number without a question is noise; a question without a number is a guess.
  </div>
  <img src="/static/decision-loop.png" alt="The question → data → decision loop" width="640">
  <ol><li>Ask</li><li>Measure</li><li>Decide</li></ol>
</section>
HTML
python3 -c 'import json,sys; print(json.dumps({"data": open("body.html").read(), "metadata": {"display_name": "Why data matters"}}))' \
  | curl "${S[@]}" -X POST "$STUDIO_URL/xblock/$HTML" -d @-

Authoring rules

The bar: every block is well written, presentable and beautiful, informative without being distracting, structured and reviewed. Written by a sub-agent? It must follow the same content brief as every other block (/iblai-api-studio "Content quality"); review it for voice, depth (no placeholder or padding text, no unit under ~150 words of real content), heading levels, the same callout/card patterns as its neighbours, and a real title — before posting, not after.

What the LMS owns — never override: font family, base font size, line height, text colour of body copy, and the component's width. No font-family, font-size on body text, max-width/width on the outer wrapper, no margin: 0 auto centring, no <style> blocks, <script>, <html>/<head>/<body>. The LMS app themes the page and adapts it to phones; a component that pins its own width or type breaks that.

What you own — use inline CSS to make content look designed: structure and emphasis inside the component. Wrap every component in one <section> (or <div>), then build with these patterns, keeping colours to a small palette (brand blue #0058cc, neutral greys, one accent for warnings):

Pattern Markup
Callout / key idea <div style="border-left:4px solid #0058cc;background:#f3f7ff;padding:12px 16px;border-radius:6px;margin:16px 0;">…</div>
Warning / common mistake same, border-left-color:#b45309;background:#fff7ed;
Card grid (2–3 concepts) <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;"><div style="border:1px solid #e5e7eb;border-radius:8px;padding:14px;"><h4 style="margin-top:0;">Title</h4><p>…</p></div>…</div>
Definition / step list <ol> with <li><strong>Step name.</strong> detail</li>
Comparison table <table style="border-collapse:collapse;width:100%;"><thead><tr><th scope="col" style="text-align:left;border-bottom:2px solid #e5e7eb;padding:8px;">…</th></tr></thead><tbody><tr><td style="border-bottom:1px solid #e5e7eb;padding:8px;">…</td></tr></tbody></table>
Tooltip / glossary term <abbr title="Full explanation">term</abbr> or <span title="…" style="border-bottom:1px dotted;">term</span>
Hierarchy inside a component <h3> for the component's topic, <h4>/<h5> for parts; never <h1>/<h2>
Emphasis <strong> for terms, <em> for nuance; colour only as a secondary cue

Use width:100% only inside the component (tables, images, iframes) — it means "fill whatever the LMS gives me", not "set the page width".

Spacing — the difference between a wall of text and a page:

  • One idea per paragraph, 2–4 sentences; a <p> per paragraph, never <br> to fake spacing.
  • A heading (<h3>/<h4>) every 150–300 words; leave the LMS's own heading margins alone.
  • Vertical rhythm with margin:16px 0 on callouts, tables, images and card grids (24px 0 before a new <h3> block if it follows a dense element).
  • Padding inside boxes (12px 16px callouts, 14px cards, 8px table cells); nothing inside a box touches its border.
  • Lists for anything enumerable; a blank line's worth of space (margin-bottom:12px) after a list before prose resumes.
  • Media on its own line with space around it (display:block;margin:16px auto; on images).
  • No empty <p></p> or &nbsp; spacers — they render inconsistently across devices.

Images and files: upload with POST /assets/{course_key}/ and reference the portable_url (/static/<filename>) — Studio rewrites it on both hosts. Always set alt (empty alt="" for decorative) and width/height when known.

Video: <div style="aspect-ratio:16/9;"><iframe style="width:100%;height:100%;border:0;" src="https://www.youtube.com/embed/<id>" title="…" allowfullscreen loading="lazy"></iframe></div>.

Links: external target="_blank" rel="noopener"; other course pages via the block's lms_url or /jump_to_id/<block_id> (relative, survives reruns).

Accessibility: one idea per paragraph, real lists for lists, tables only for tabular data with <th scope="col">, ≥ 4.5:1 contrast on anything coloured, no meaning by colour alone, headings in order.

Size and validity: under ~30 KB per component including inline SVG and any data: image URIs — an inline chart or diagram easily runs to hundreds of KB and is the usual way this limit is blown. Upload images and SVGs as course assets (POST /assets/{course_key}/) and reference /static/<file>; split long readings across components or units. Studio stores exactly what you send, so validate the fragment yourself — one unclosed tag breaks the unit's layout. \( … \) / \[ … \] LaTeX is rendered by MathJax.

Notes

  • display_name defaults to "Text" when never set (the bulk builder in /iblai-api-studio-outline leaves it so) — give every block a title; it shows in the page header and analytics.
  • Updating data on a published block makes the unit has_changes: true; republish the unit.
  • The stored HTML is returned verbatim by GET /xblock/{html} — read before a partial edit and resend the full body.
  • One html block per idea; several html blocks in a unit are fine (unlike problem blocks — see /iblai-api-studio-problem).
  • Boilerplates other than announcement.yaml exist upstream (raw.yaml, latex_html.yaml, zooming_image.yaml) but only announcement.yaml is verified here; an unknown name creates a blank block without error.