---
name: figma-to-content
description: "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."
license: Apache-2.0
metadata:
  version: "1.0.0"
---

# 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-sect