# Figma To Content

> Use this to turn a Figma design into an AEM Edge Delivery Services (EDS / AEM / Franklin / Helix) content page in Document Authoring (DA, da.live). Triggers: "build this Figma frame in EDS", "turn this Figma design into a DA page", "publish this design to da.live", or providing a figma.com URL for a page. Reads the frame (and any annotations) via a Figma MCP, resolves each section to an existing block, a new isolated block, or default content (inferred against the project's existing blocks and confirmed with you, or read from annotations when the frame happens to have them), generates DA-compliant body-fragment HTML, and deploys via the DA Source API + preview.

- Skill: `adobe/figma-to-content` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add adobe/figma-to-content`
- Raw SKILL.md: https://api.skillmd.com/api/skills/adobe/figma-to-content/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- License: Apache-2.0
- Author: Adobe (https://skillmd.com/u/adobe), verified publisher
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/adobe/figma-to-content

---


# figma-to-content — Figma design → EDS content page in DA

Read a Figma frame, assemble a page from EDS blocks and default content, and
publish it to Document Authoring. Runs with a **Figma MCP** (to read the
design) and a **DA IMS token** (to write content) — no proprietary tooling
required.

This skill **orchestrates existing skills**; it does not reimplement DA rules,
block knowledge, or block-building. **Invoke those skills — do not inline them.**
The condensed rules quoted in this file are *pointers* to jog the right skill,
never a substitute for loading it: when a phase names a skill, invoke it and work
from its actual guidance. Running this file as a self-contained procedure —
hand-writing blocks, authoring DA HTML from memory, skipping the browser/visual
check — is the single most common way a run goes wrong. Phase 2.3 turns the
confirmed plan into an explicit manifest of the sub-skills you must invoke.

## Two paths

Classify each section of the design, then follow the matching path:

- **Content only** — every section maps to a block that **already exists** in
  the target project, or to **default content** (plain headings/paragraphs/
  images/buttons — no block). Author content and deploy. No code changes.
- **Content + code** — a section needs a block the project **does not have**,
  or an existing block matches structurally but its **styling diverges** (the
  look lives in block-specific CSS you'd have to edit). Create it as a **new,
  isolated block** (via the block-building skills), push the code, then author
  content and deploy. **Never skin an existing block or add per-section rules
  to global CSS** — new, additive blocks only. (Retargeting the project's
  global design tokens is a separate, allowed theming step; see Guardrails.)

A single design usually mixes all three (known blocks + default content + one
or two new blocks).

## When to use

- The user has a Figma frame representing a page and wants it as an EDS page in DA.
  The common case: a customer **already on EDS**, with their own blocks, gets a
  new design for a new page — some sections reuse existing blocks, some need new
  ones.
- **The default path is infer-and-confirm.** Usually the frame is **not**
  annotated (e.g. the user just says "migrate this page"): the skill **infers**
  each section's mapping against the project's existing block palette and
  **confirms the plan** before building, asking whenever a section is ambiguous
  (Phase 2). This path needs nothing but the design itself.
- **Annotations are an optional accelerator — never required.** If a frame
  happens to declare each section's block / default content / new block (see
  [references/annotation-contract.md](./references/annotation-contract.md)),
  those declarations are taken as authoritative and skip the inference for that
  section. Absent them, nothing is lost — the skill infers and confirms.

### When NOT to use

- **Redesign / restyle an existing EDS site**, or convert arbitrary generated
  static HTML (Mobirise, Relume, v0, exported Figma HTML). Use **snowflake**.
- **Universal Editor or AEM Cloud Service (Java/OSGi/JCR).** Out of scope.

## Related skills — orchestrated by this one

| For | Use skill |
|---|---|
| DA IMS token (`DA_TOKEN`) | **da-auth** |
| DA body-fragment HTML rules, Source API, preview/publish, media | **da-content** |
| Whether a block exists + its authoring model & examples | **block-collection-and-party** |
| Surveying the whole available block palette | **block-inventory** |
| Designing a content model for a **new** block | **content-modeling** |
| Building a **new** block (full dev workflow) | **content-driven-development** (invokes **building-blocks**, **testing-blocks**) |
| Rendering a block + **visual comparison to the design** (the reuse gate) | **testing-blocks** (browser/Playwright screenshot + "compare implementation to design") |

The DA-write contract in Phase 5 is the same one **da-content** documents
(see its `references/html-content.md` and `references/platform.md`).

---

## Inputs (gather before Phase 1; ask if missing — never guess)

- **Figma reference** — file key + node id of the page frame (from the
  figma.com URL or the current Figma MCP selection). A file usually holds
  **many frames** — desktop/mobile variants, A/B versions, work-in-progress
  copies of the same page. Confirm **exactly which frame** to build; don't
  assume the first or largest. Two frames that are variants of the *same* page
  are one page, not two — ask which is canonical rather than deploying both.
- **Target project** — a local checkout of the EDS project repo (needed to see
  existing blocks under `blocks/`, and required for the content+code path to
  add block code). Its GitHub `{owner}`/`{repo}` and the deploy `{branch}`.
- **DA location** — `daOrg`, `daRepo` (the DA namespace), page `PATH` (no
  extension, lowercase/dash only — see da-content platform rules). In the
  standard EDS+DA setup `daOrg`/`daRepo` **equal** the GitHub `{owner}`/`{repo}`;
  confirm, because Phase 5 writes to `daOrg`/`daRepo` but previews/renders on
  the GitHub `{owner}`/`{repo}`/`{branch}`.
- **`DA_TOKEN`** — via **da-auth**, which exports `$DA_TOKEN` and caches it at
  `~/.aem/da-token.json` (valid ~1h). Prefer the `$DA_TOKEN` da-auth already set
  in this session; read the cache file only if that's unset. Two distinct
  failures: a `401` with an empty body means the token **expired** → re-auth; a
  cache file that **can't be read because the execution sandbox has no `$HOME`
  access** means the token is *unreachable*, not expired (see Phase 0 step 3) —
  don't conflate them.

---

## Phase 0 — Preflight (fail fast, before any read or write)

Verify the run can actually complete **before** reading the design or writing to
DA — a missing prerequisite caught here is one actionable message; caught mid-run
it is a confusing, half-built page. Run these checks in order and, on the first
that fails, **stop with the specific remediation below** — do not proceed on a
guess or a partial capability.

1. **Figma MCP reachable.** Confirm a Figma MCP is connected and responds via a
   cheap call (e.g. `whoami`, or listing its tools). If **no Figma MCP tool is
   available at all**, stop: *"No Figma MCP is connected. Connect one (Claude
   desktop Dev Mode, an IDE Figma integration, or a remote Figma MCP) and
   re-run."* Record the authenticated identity (`whoami`) for the next check.
2. **Access to the specific file.** Make one lightweight call against the target
   `fileKey` (e.g. `get_metadata` scoped to the frame, or `get_design_context`
   on the node). A **permission / not-found** error (`403`/`404`/"no access")
   means the file is not shared with the authenticated account → stop: *"Figma
   reports no access to `<fileKey>` as `<whoami>`. Share the file with that
   account, switch accounts, or provide a file you can open."* **Distinguish this
   from a transport cap** — a truncated, garbled, or JSON-parse-error response is
   the size cap (see Phase 1), **not** an access failure: retry narrower, do not
   report it as no access.
3. **DA write path available.** Confirm a `DA_TOKEN` is obtainable via **da-auth**
   — prefer the `$DA_TOKEN` it exports into the session, else its cache at
   `~/.aem/da-token.json`, else freshly minted. **If the cache exists but can't be
   read because this execution sandbox has no `$HOME` access**, the token is not
   missing — it is *unreachable*; do **not** loop re-minting. Stop with that
   distinction spelled out: *"A DA token exists but this sandbox can't read
   `~/.aem/da-token.json` — run where the cache is readable, or provide the token
   as `$DA_TOKEN` (or a readable path)."* If no token can be obtained at all,
   stop: *"Can't obtain a DA token (da-auth) — authenticate to DA and re-run."*
   Either way, don't spend a full Figma read only to fail at the deploy step.
4. **Project checkout + orchestrated skills present.** The target repo is checked
   out locally (needed to see `blocks/` and to add new-block code) and the skills
   this one orchestrates (**da-auth**, **da-content**, the block skills) are
   available. If the checkout path is unknown, ask for it.

On all-pass, print a one-line preflight summary — Figma identity, the file/frame,
and the DA `org/repo` + `branch` you will write to — then proceed to Phase 1.

---

## Phase 1 — Read the Figma design (Figma MCP)

Use a Figma MCP (Claude desktop / IDE / external). **Introspect the actual tool
schemas** — signatures differ between MCP implementations (the local Dev Mode
server often works off the current selection and may not take a `fileKey`; the
remote/desktop server takes `fileKey` + optional `nodeId`). The tools you need,
by capability:

- **Structure** (e.g. `get_metadata`) — the frame's section/layer tree; node
  ids, names, positions, sizes. Derive the section list from the **content
  groups** in visual order (sort by `y`) — **not** the raw child list: full-
  bleed background rectangles, overlays, and decorative shapes are *part of* a
  section (its background), not sections of their own, and a single visual
  section is often split across sibling nodes (e.g. a background rect + a tab
  strip + a text group). Ignore the decorative layers and group the rest into
  sections by position. Usually `fileKey` required, `nodeId` optional. **Some
  MCP servers cap response size — even a single frame's structure dump can
  exceed it; scope the call to the frame or, if that still fails, one section
  at a time. A truncated, garbled, or JSON parse-error response *is* the cap
  being hit — retry narrower; do not read it as "no structure."**
- **Visual** (e.g. `get_screenshot`) — a per-section reference image to
  sanity-check the block/content mapping.
- **Content & assets** (e.g. `get_design_context`) — text, links, and image
  asset download URLs for a node. For the content+code path this also provides
  the layout/structure a new block must reproduce. **Request the lean form** —
  exclude the screenshot from the context call (fetch visuals separately with
  the screenshot tool) and disable any Code Connect lookup (e.g.
  `excludeScreenshot` / `disableCodeConnect`-style options) unless you are
  mapping to a real component library; both add payload and round-trips and can
  push a large response over the transport cap. Icons are usually **component
  instances**, not raster fills — obtain their **SVG** (export/copy as SVG),
  never a PNG, for the `/icons` or DA `/media` reference in Phase 4.
- **Design tokens** (e.g. `get_variable_defs`) — colors, spacing, type. Read
  annotation values and, for new blocks, source token values.

**Call budget & order — Figma MCP calls are rate-limited and payload-capped, so
spend them deliberately rather than re-fetching:**

1. **`get_metadata` first** (scoped to the frame) — the structure/section tree.
   The cheapest orienting call; every later call keys off the node ids it returns.
2. **`get_screenshot` of the whole frame early** — one full-frame reference image
   up front is the anchor you reconcile the section count against (segmentation
   heuristic) and, later, compare the rendered page to (Phase 5 Stage B). Take
   per-section crops afterwards, only for the sections you actually build.
3. **`get_design_context` targeted and lean, per section** — request the lean
   form (exclude the screenshot, disable Code Connect) and scope it to **one
   section's node at a time**. A whole-frame context dump is the single call most
   likely to blow the transport cap.
4. **Asset download last** (`download_assets` / export-as-SVG) — only for the
   assets the **confirmed** plan references, after Phase 2. Don't pull binaries
   for sections that end up reusing an existing block or being cut.

A `429`/rate-limit or a truncated/garbled response is a **budget/cap signal, not
"no data"**: back off, narrow the scope (frame → section), and retry — never read
it as an empty design or as missing access (Phase 0 step 2 draws the same line).

Produce an ordered **section inventory**: `{ sectionNodeId, annotation,
screenshot, content, background }` — capture each section's **background /
theme** (e.g. alternating light and dark sections), because the global token
retheme (Guardrails) recolors blocks but does **not** switch a section's
background: that carries via a `section-metadata` `Style` class or a block's
own defined dark/light variant (Phase 4). Read annotations per
[references/annotation-contract.md](./references/annotation-contract.md).

> **Segmentation heuristic** — when the frame has no explicit grouping, derive
> the section list like this, not from the raw child order:
> 1. Sort the frame's direct children by `y` (top to bottom).
> 2. **Drop pure-decoration layers** from the section list — full-bleed
>    background rectangles, gradients, blurs, absolutely-positioned shapes with
>    no text or interactive child. Record each as the *background* of the
>    content it sits behind (→ Phase 4 `section-metadata`); don't emit it as a
>    section of its own.
> 3. **Merge siblings that form one visual band** — nodes whose vertical
>    extents overlap or sit within ~one line-height of each other (a background
>    rect + a heading group + a button row are *one* section, not three).
> 4. **Reconcile the count against the screenshot** before resolving: the eye
>    sees the real sections; a mismatch means you over- or under-split — fix it
>    before Phase 2.

> **Placeholder content is common — don't ship it.** Designs routinely contain
> dummy copy (`Lorem ipsum`, a CTA literally labelled "Button" or "Lorem
> Ipsum", the same card title repeated across every card) and unfilled slots
> (empty or transparent image cells, blank stat boxes). Author from the **real
> text and media in the design context** — not from the placeholder and not from
> invented filler. Where it's clearly placeholder, **flag it in the plan and
> confirm the real copy/media with the user** rather than publishing "Lorem
> Ipsum" to a live page. Distinct items (cards, tabs, news entries) need
> **distinct** copy and images — repeated-identical content is itself a
> placeholder smell. If the design *itself* carries only placeholder, you cannot
> manufacture the real content: stop and get it from the user before publish.

> Site chrome (nav bar, footer) is usually **not page body** — in EDS it is
> sourced from separate `/nav` and `/footer` documents via the header/footer
> blocks. Don't author it into the page unless the user asks.

---

## Phase 2 — Resolve each section

Every section resolves to exactly one of: **existing block** (→ 3A),
**default content** (→ 3C), or **new block** (→ 3B). How that decision is
reached depends on whether the section is annotated.

### 2.0 — Know the project's block palette (always)

Before resolving anything, enumerate what the project **already has**:
`ls -d blocks/*/` plus **block-inventory** / **block-collection-and-party** for
each block's **authoring model** (row/cell structure, variants) **and a
rendered example** — the block's `liveExampleUrl` when it comes from the Block
Collection, or the project's own block rendered at `localhost:3000`. That
rendered example is the "block side" of the 2.1 / Phase 3A visual check. This
is the reuse-candidate set — essential when the customer is already on EDS with
their own blocks.

### 2.1 — Resolve each section (annotation-first, else infer)

**If the section is annotated** (see
[references/annotation-contract.md](./references/annotation-contract.md)), the
annotation is **authoritative**: named block that exists → existing block (3A);
marked `new` (or absent-and-user-confirmed) → new block (3B); plain prose/media
→ default content (3C).

**If it is not annotated** (e.g. "just migrate this page"), **infer** the
mapping — do not dump it as unresolved:

1. Plain prose/media (headings, paragraphs, images, a standalone link) with no
   repeating structure → **default content** (3C).
2. Otherwise match it against the 2.0 palette using the **reuse gate (structure
   AND visual, Phase 3A)**: does its content model fit an existing block *and*
   does that block's rendered example — under the project theme — look like the
   section, allowing only token differences and variants the block defines?
   - **Both fit → existing block** (3A).
   - **Structure fits but the look diverges** (bespoke card/layout/decoration
     the block's CSS can't produce without editing it), **or nothing fits →
     new block** (3B).
   - **A section carrying an interactive control** — tabs / segmented switch,
     accordion, carousel or slider, toggle — is structural divergence no static
     block reproduces: route it to a **new block** (3B), or, if the control is
     non-essential chrome, **confirm with the user** whether to keep it or
     flatten it to static content. Don't silently drop the interaction or fake
     it with a look-alike static block.
3. Attach a **confidence** to every inference: `high` (clear reuse match, or
   clearly novel) or `low` (structure fits but styling is borderline; two
   blocks plausibly fit; new-variant-vs-new-block; content model ambiguous).

### 2.2 — Confirm the plan before deploying (never deploy a guess)

Present a **resolution plan** — one line per section: decision (reuse `X` /
default content / new block `Y`), confidence, a one-clause rationale, and a
**content flag** on any section whose copy or media is placeholder (Phase 1)
and needs real content before publish.

- **High-confidence sections auto-proceed through building** (Phases 3–4) —
  don't block on them.
- **Stop and ask before building** any `low`-confidence section or genuine
  ambiguity, offering the concrete choice (reuse this block vs. new block;
  which block; new variant vs. new block). Wait for the answer.
- **Pause once before deploying (Phase 5)** whenever the plan contains any
  **inferred** (unannotated) mapping: show the final plan and get a single
  confirmation before the da.live write/preview — deploy is outward-facing and
  hard to reverse. Skip this pause only if the user pre-authorized an
  unattended run. A **fully annotated** plan needs no pause — the annotations
  are the authorization.
- **Flag an existing target page.** Before confirming, check whether the target
  `content/<PATH>.html` already exists in DA (a cheap Source-API `GET`, Phase 5);
  if it does, deploying **overwrites** it — say so in the plan and get explicit
  overwrite confirmation. Never silently clobber a page you didn't create, even
  on an otherwise pre-authorized unattended run. **Record two facts per path** for
  Phase 5 to enforce: `PLANNED_STATE` (`new` if the check returned 404, `exists`
  if 200) and `OVERWRITE_OK` (`yes` only when the user confirmed overwriting an
  existing page). Phase 5 re-checks existence right before writing and **refuses**
  if the state changed since planning (a page appeared in the gap) or overwrite
  was never confirmed — the plan-time check alone is not a license to clobber.
- The user can override any line.

Never silently drop a section, and never deploy an **inferred** mapping the
user has not seen.

**Worked example** — an unannotated 4-section frame; this is the plan you
present in 2.2 (one line per section):

| # | Section | Decision | Conf. | Why | Content |
|---|---|---|---|---|---|
| 1 | Hero band — heading + 2 CTAs over a photo | reuse `hero` | high | model fits; heading **and** CTAs stay legible on the media under the theme | ok |
| 2 | 3 feature blurbs — icon + title + text | reuse `cards` | high | content model and rendered look both fit | ok |
| 3 | Metric strip — 3 big numbers + labels | **new block** `stat-cards` | high | bespoke panel look no existing block produces (3B) | ok |
| 4 | Newsletter row — heading + email field + button | **new block** / confirm | low | carries an interactive control (input) — ask keep vs. flatten (G5) | ⚠ placeholder copy |

Then act on it: sections 1–2 build without blocking; #3 builds (high-confidence
new block); **#4 stops for a decision** (low-confidence + interactive control);
and because the plan contains inferred mappings, the whole thing gets **one
pre-deploy confirmation** before the da.live write. Section #4's ⚠ flag means
its real copy must be supplied before publish, not shipped as placeholder.

### 2.3 — Lock the orchestration manifest (which sub-skills this plan requires)

Turn the confirmed plan into an explicit **manifest** of the sub-skills it
requires and **invoke each one** — this is where the intro's *orchestrate, don't
inline* rule becomes a concrete, ticked list. This file's summaries never
substitute for loading the named skill.

Derive the manifest from the plan:

| The plan contains… | You MUST invoke |
|---|---|
| **Any** section (always) | **da-auth** (token) and **da-content** — load its real `references/html-content.md`, `platform.md`, and `media.md`, *not* the condensed rules in this file — before authoring (Phase 4) and deploying (Phase 5). |
| An **existing-block reuse** (3A) | **block-collection-and-party** (authoring model + a rendered example) **and testing-blocks** for the visual reuse gate (rendered block vs. the Figma section screenshot). |
| A **new block** (3B) | **content-modeling** (design the authoring model), then **content-driven-development** (which runs **building-blocks** and **testing-blocks**). Do **not** hand-write block JS/CSS from this file. |
| **Default content** (3C) | **da-content** only (no block skills). |

Record the manifest as an evidence-bearing checklist and tick each item **only
after you actually invoked the skill** — "I know what it does" is not invocation,
and an un-invoked required skill means this phase is **not complete**:

- [ ] **da-content** reference docs loaded (`html-content.md` / `platform.md` / `media.md`)
- [ ] **block-collection-and-party** invoked for every reused block *(if any 3A)*
- [ ] **content-modeling** + **content-driven-development** invoked for every new block *(if any 3B)*
- [ ] **Default-content** sections authored via **da-content** alone — **no** block-building skills invoked for them *(if any 3C)*
- [ ] **testing-blocks** invoked — its browser render + visual comparison **is** the
      Stage B pre-publish check (Phase 5); a run with **no** browser available
      reports the page **preview-only, UNVERIFIED**, never "done".

If the environment genuinely cannot run a required skill (e.g. no browser for
testing-blocks), **say so explicitly in the report and mark the affected checks
unverified** — never silently substitute this file's summary and call it passed.

---

## Phase 3A — Map content into an EXISTING block

**Reuse gate — structure AND visual.** An existing block is a valid target
only when the section both (a) **fits the block's authoring model** (its
row/cell structure and field types) *and* (b) **matches the block's rendered
appearance** under the project theme, using only tokens and variants the block
already defines. Structural fit alone is **not** enough: if the section's
visual identity — bespoke layout, corner radius, shadow, decorative treatment —
lives in that block's own CSS, you cannot reproduce it without editing the
block (forbidden), so route the section to **Phase 3B** (new block). Global,
token-level differences (palette, fonts, type scale) do **not** break reuse —
they are absorbed once by retargeting the project's design tokens (see
Guardrails). **How to run the visual check — reuse testing-blocks, don't invent
one:** get a rendered example of the candidate block — its `liveExampleUrl`
(block-collection-and-party / block-inventory) or the project's own block
rendered at `localhost:3000` with the section's **actual** content — including
secondary text, captions, and CTAs over whatever background or media the block
places them on, not just placeholder cells — then follow **testing-blocks**'
browser/Playwright-MCP screenshot pass (mobile/tablet/desktop) and its "compare
implementation to design" step, comparing that screenshot against the Figma
**section screenshot** from Phase 1. Watch for treatments a block applies to
only its primary element: one that (say) whitens a heading over dark media but
leaves the supporting text and buttons at body color passes a structural check
yet renders that text illegibly — a divergence the token retheme cannot fix.
Divergence beyond what the token retheme explains ⇒ new block (or a new
variant), not reuse. This outcome is **blocking**: the section is not resolved
until its rendered look — that text included — is faithful, and the fix is a new
isolated block/variant, never an edit to the shared block. Recording the gap in
the plan and reusing the block anyway is a **plan note, not a fix** — the Phase 5
pre-publish gate treats such a box as failed.

Once the gate passes, **invoke block-collection-and-party** to learn the block's
authoring model (its examples show the row/cell structure and variants) — read
the block from the skill, don't guess its model from its CSS source. Then pour
the Figma content into that structure:

- **Text** → matching cells; preserve heading levels from the design.
- **Variants** → extra class tokens on the block (e.g. `cards highlight`).
  Only apply a variant the block actually defines. (Adding a *new* variant =
  modifying an existing block = Phase 3B, not 3A.)
- **Links/buttons** → a **standalone link** (the only content of its
  paragraph) auto-promotes to a button; wrap in `<strong>` for a primary
  button, `<em>` for secondary. Do not add `target="_blank"` (decoration
  handles external links). Validate the href's URL scheme and escape the
  link text/attributes before emitting — see Phase 4, *Sanitize everything
  derived from the design*. *(da-content html-content.md §8)*
- **Images** → Phase 4 (they need real URLs).

---

## Phase 3B — Create a NEW block (content + code)

Only for sections Phase 2 routed here (a needed block is missing, or an existing
block's look diverges) — the **3B** case. **Guardrails (strict):**

- Create **new, isolated block folders** only (`blocks/<new-name>/`).
- **Do NOT** skin this block by editing an existing block, `scripts.js`, or
  `head.html`, or by adding block-specific rules to global CSS — keep it
  self-contained under `blocks/<new-name>/`. *(Retargeting the project's
  global design tokens in `styles/styles.css` — the `:root` custom properties
  and base typography — is a separate, allowed project-theming step, not part
  of building this block; see Guardrails.)*
- New block **names and variant tokens** must obey EDS block-name rules
  (da-content html-content.md §3.3): lowercase alphanumeric + single hyphens,
  **no underscores, no double dashes, must not start with a digit**
  (`pricing-table` ✓, `pricing_table` / `2col` / `promo--wide` ✗). Names must
  be unique and not collide with existing blocks.

**Build route — invoke content-driven-development (don't hand-write the block).**
Build every new block by **invoking content-driven-development** — not by writing
block JS/CSS from scratch off this file's summary. It invokes **content-modeling**
(design the authoring model from the Figma structure/tokens) then
**building-blocks** and **testing-blocks**, and produces a self-contained
`blocks/<name>/` — no source URL, no installed substrate, no page chrome, and no
global styles. That is the route that honors the 3B guardrails above, and its
testing-blocks pass is the block's Stage B verification (Phase 5). Build a **bespoke, one-off** section
the same way — it is still an ordinary isolated block, and "one-off" changes
nothing about how it is generated.

> **Do not use snowflake here.** Snowflake converts an *already-rendered* page:
> it requires a reachable **Source URL**, **installs an overlay substrate** into
> the repo, and in block mode emits **header/footer fragments and global
> styles/tokens** — each of which violates this skill's constraints (isolated new
> block, don't touch globals, work from the **Figma frame**, not a live URL).
> Snowflake is the right tool for a *different* entry point — converting an
> existing static/rendered site — as noted under "When NOT to use".

Use the Figma design context/tokens from Phase 1 as the source of truth for
layout and styling. New-block **CSS must target structure, not authored
classes** — inline wrappers like `<span class="…">` are stripped inside block
cells at delivery (da-content html-content.md §3.9), so a class you emit in a
cell will not survive.

**Make the block responsive.** A Figma page frame is almost always a single
**desktop** width, but EDS pages are responsive. Author the block mobile-first
(or with explicit breakpoints) so a multi-column layout collapses to one column
on narrow viewports, and verify at mobile / tablet / desktop via
**testing-blocks** — don't ship a fixed desktop-width block. If the design
provides a **separate mobile frame**, use it to derive the breakpoint behavior
(what stacks, what hides, how type scales) — it's the *same page*, so it feeds
one responsive block, **not** a second page (see Inputs on frame variants).

The new block's code must be **committed and pushed to the deploy branch on
GitHub and built by Code Sync** before the page can render it — see Phase 5
(content+code).

---

## Phase 3C — Author DEFAULT CONTENT (no block)

For sections Phase 2 routed to default content — the **3C** case — emit standard
document elements directly inside the section `<div>` (see Phase 4 skeleton) — no
block wrapper:

- Headings `<h1>`–`<h6>` (preserve levels), paragraphs, lists, images.
- A **standalone link** in its own `<p>` becomes a button (`<strong>`/`<em>`
  for primary/secondary) — same rule as 3A.
- Do **not** add `class`, `id`, or `style` — decoration adds them at delivery.

*(da-content html-content.md §6)*

---

## Phase 4 — Generate DA body-fragment HTML (da-content)

Emit a **body fragment** (not a full HTML document) per **da-content**. **Invoke
da-content and load its `references/html-content.md`, `platform.md`, and
`media.md` now** — the rules quoted throughout this phase are reminders to jog the
right skill, not the source of truth. Subtle authoring rules (block-cell inline-
tag normalization, media MIME/extension derivation, metadata keys) live in those
docs; authoring from this summary alone is how they get missed. Write one file per
page to `content/<PATH>.html`.

**Mandatory skeleton** (da-content html-content.md §1–§2): wrap everything in
`<body>` with an (empty) `<header>`/`<footer>` and a `<main>`; **each section
is exactly one `<div>` directly inside `<main>`** — the `<div>` *is* the
section boundary (no `<hr>`). Do NOT emit `<!DOCTYPE>`, `<html>`, `<head>`,
`<script>`, `<style>`, `style=`, or `class=` on default content.

```html
<body>
  <header></header>
  <main>
    <div>
      <!-- section: default content and/or a block, in visual order -->
      <h1>Heading</h1>
      <p>Intro paragraph.</p>
      <div class="block-name variant">
        <div><div>cell</div><div>cell</div></div>
      </div>
    </div>
    <div>
      <!-- next section -->
    </div>
  </main>
  <footer></footer>
</body>
```

- **Sanitize everything derived from the design — text, attributes, links.**
  Figma text and layer names are untrusted input to the HTML you emit; treat
  them as data, never as markup:
  - **HTML-escape** every design-derived string before it lands in the document
    — `&`→`&amp;`, `<`→`&lt;`, `>`→`&gt;`, and inside attribute values also
    `"`→`&quot;` and `'`→`&#39;`. A heading `Tips & Tricks <Beta>` must serialize
    as `Tips &amp; Tricks &lt;Beta&gt;`, never as raw markup that can break the
    document or inject an element.
  - **Validate every link's URL scheme** against an allowlist — `http`, `https`,
    `mailto`, `tel`, or a root-relative (`/…`) path. **Reject `javascript:`,
    `data:`, `vbscript:`, and any other scheme** (a prototype link can carry
    anything): drop the href or ask the user — never emit it.
  - **Admit a Figma-derived class token only after block-name validation** — a
    layer/frame name becomes a block or variant class *only* once it passes the
    EDS name rules in Phase 3B (lowercase alphanumeric + single hyphens, no
    underscores/double-dashes, not digit-initial); never pass a raw layer name
    through as a class.
- **Blocks — canonical div form:** `<div class="block-name variant">`, each
  direct child `<div>` a row, each grandchild `<div>` a cell. The first class
  token is the block name (resolves to `blocks/<name>/<name>.{js,css}`).
  Multi-word variants hyphenate; multiple variants are separate class tokens.
  Max 4 cells per row; blocks cannot nest. *(html-content.md §3)*
- **Default content:** headings/paragraphs/lists/images/buttons live directly
  in the section `<div>`, outside any block. *(html-content.md §6)*
- **Icons — two non-interchangeable paths; never a stand-in glyph.** The
  `<span class="icon icon-<name>"></span>` convention resolves **only** to the
  project's Code Bus `/icons/<name>.svg`, so that SVG must be **committed to the
  repo `/icons/` folder and pushed on the deploy branch** (content+code path,
  same as block code) and return `200` on the branch host — uploading it to DA
  `/media` does **not** satisfy the span (it 404s and the icon silently vanishes).
  A DA-`/media` SVG must instead be referenced by **full URL on an `<img>`**, not
  an icon span. Get the real **SVG** in Phase 1; **never emit an emoji or Unicode
  glyph in place of a designed icon.** *(html-content.md §7)*
- **Images — MUST be full, fetchable URLs.** Figma render URLs expire, and
  **repo-relative paths (`/img/…`) render as `about:error`.** So: download the
  image bytes from Figma (Phase 1 asset URLs), **upload each binary to DA**
  (`PUT admin.da.live/source/{daOrg}/{daRepo}/<media-path>`), and reference
  `https://content.da.live/{daOrg}/{daRepo}/<media-path>`. External image URLs
  are also accepted (the preview sideloads them). Author a bare `<img alt="…">`
  and let the pipeline build the `<picture>`.
  - **Normalize format, extension, and MIME together — from the bytes, never the
    URL suffix.** Detect the real format from the image's magic bytes (or the
    asset's reported `format`), then make **all three agree**: the multipart
    `type=` MIME, the `<media-path>` file extension you PUT to, and the extension
    in the `content.da.live` URL you author. Design tools routinely export JPEG
    bytes under a `.png`-named asset; trusting the suffix gives you a `.png` path
    served as `image/jpeg` (or the reverse) — a latent corruption bug. A layer
    that *looks* vector (an icon, a logo, a shape) often comes back **rasterized**
    — `download_assets` returns it under `rawImages` with `svgAssets` empty — so a
    design that implies `.svg` can hand you PNG/JPEG bytes. Author each `<img>`
    extension from the bytes you actually downloaded, never from the layer's
    apparent type or name. Canonical
    mapping: JPEG→`.jpg`/`image/jpeg`, PNG→`.png`/`image/png`, WebP→`.webp`/
    `image/webp`, GIF→`.gif`/`image/gif`, SVG→`.svg`/`image/svg+xml`. If bytes
    and asset-reported format disagree, trust the bytes.
  *(html-content.md §9 + media.md)*
- **Section styling** → a `section-metadata` block **inside** the section
  (`Style` → CSS classes; other rows → `data-*`). *(html-content.md §4)*
- **Page metadata** → a single `metadata` block (exact class), placed as the
  **last element of the last section inside `<main>`** (never after `</main>`
  or in `<footer>`); keys like `title`, `description`, `image`, `template`,
  `theme`. **Author it from the design — don't leave it empty or a bare
  comment.** Derive `title` from the frame name or the page `<h1>`, `description`
  from the hero/intro copy (a concise real sentence, never lorem), and `image`
  from the primary/hero image's uploaded DA URL when the design has one. This
  block is **required** — the Phase 5 pre-publish gate blocks on its absence — so
  populate it rather than deferring it. If the design offers no usable
  title/description text, ask the user rather than inventing marketing copy.
  *(html-content.md §5)*

Inside block cells the pipeline runs a stricter inline-tag normalization than
for default content — `<span class>` is unwrapped (class lost), `<b>`→`<strong>`,
`<mark>`→`<em>`, etc. Restrict cell content to the html-content.md §3.9 preserve
list. A wrong metadata **key** or block **field** silently corrupts output;
when unsure, read da-content.

---

## Phase 5 — Deploy to DA

**If a DA MCP server is available in the session, use its tools** for auth and
source writes (da-auth and da-content both defer to it when present).
Otherwise use the Source API directly, below.

```bash
# Two identities — keep them separate. DA (Document Authoring) and GitHub are the
# same org/repo in the standard EDS setup, but nothing guarantees it, so never
# assume one from the other. DA endpoints (admin.da.live/source, content.da.live,
# da.live/edit) use the DA pair; the render host and admin.hlx.page (code, preview,
# live) use the GitHub pair.
DA_ORG=<da-org>        # Document Authoring org
DA_REPO=<da-repo>      # Document Authoring repo/site
GH_OWNER=<gh-owner>    # GitHub owner
GH_REPO=<gh-repo>      # GitHub repo
# In the standard setup all four match: DA_ORG=GH_OWNER=<owner>, DA_REPO=GH_REPO=<repo>.
BRANCH=<branch>        # git deploy ref (usually main). For content+code this MUST be
                       # the branch the new-block code was pushed to and Code Sync built.
BRANCH_HOST=${BRANCH//\//-}   # host label: slashes → dashes ('feature/x' → 'feature-x').
                              # Used BOTH for the aem.page/aem.live hostname AND as the ref
                              # segment in every admin.hlx.page path (code/preview/live): that
                              # ref is a SINGLE path segment, so a slashed branch ('figma/x')
                              # splits it and 404s — pass the dashed label ('figma-x'), which is
                              # what AEM actually resolves. Only git itself (push/checkout) uses
                              # the literal slashed $BRANCH. For a slash-free branch the two
                              # forms are identical, so $BRANCH_HOST is always the safe choice
                              # for admin.hlx.page.
P=<path-without-extension>
TOKEN="$DA_TOKEN"      # from da-auth; 401 w/ empty body ⇒ expired, re-auth

# Fail fast if the branch host would be unresolvable (>63 chars won't resolve).
host="$BRANCH_HOST--$GH_REPO--$GH_OWNER"
[ "${#host}" -le 63 ] || { echo "❌ branch host '$host' is ${#host} chars (>63) — won't resolve; use a shorter branch/repo/org"; exit 1; }

# --- checked-request helper: every call asserts its status; a bare `curl -sS`
#     exits 0 on 401/403/409/5xx, so an unchecked curl silently "succeeds" on a
#     failed write. req <expected-codes> <curl-args…>: prints the body, retries a
#     few times on network/429/5xx, and aborts (non-zero) on any other mismatch.
#     Use it for every PUT/POST below; if a DA MCP server is used instead, apply
#     the same rule — assert the returned status, don't assume success. ---
req() {
  local expect="$1"; shift
  local attempt out code body
  for attempt in 1 2 3 4 5; do
    if out=$(curl -sS -w $'\n%{http_code}' "$@"); then code="${out##*$'\n'}"; else code="000"; fi
    body="${out%$'\n'*}"
    case ",$expect," in *",$code,"*) printf '%s' "$body"; return 0;; esac
    case "$code" in
      000|429|5??) sleep $((attempt * 2)); continue;;   # transient — bounded retry
      401)         echo "❌ 401 (empty body ⇒ token expired) — re-auth (da-auth) and retry" >&2; return 1;;
      *)           echo "❌ HTTP $code (expected $expect) — $*" >&2; return 1;;   # 4xx: do not retry
    esac
  done
  echo "❌ giving up after retries (last status $code) — $*" >&2; return 1
}

# --- content+code path ONLY: block code must be LIVE before the pag

…(truncated)
