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),
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.
- 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.
- 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.
- 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.
- 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:
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.
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.
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.
- 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.
Segmentation heuristic — when the frame has no explicit grouping, derive
the section list like this, not from the raw child order:
- Sort the frame's direct children by
y (top to bottom).
- 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.
- 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).
- 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), 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:
- Plain prose/media (headings, paragraphs, images, a standalone link) with no
repeating structure → default content (3C).
- 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.
- 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:
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.
<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
—
&→&, <→<, >→>, and inside attribute values also
"→" and '→'. A heading Tips & Tricks <Beta> must serialize
as Tips & Tricks <Beta>, 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.
# 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)
1---2name: figma-to-content3description: 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.4license: Apache-2.05---67# figma-to-content — Figma design → EDS content page in DA89Read a Figma frame, assemble a page from EDS blocks and default content, and10publish it to Document Authoring. Runs with a **Figma MCP** (to read the11design) and a **DA IMS token** (to write content) — no proprietary tooling12required.1314This skill **orchestrates existing skills**; it does not reimplement DA rules,15block knowledge, or block-building. **Invoke those skills — do not inline them.**16The condensed rules quoted in this file are *pointers* to jog the right skill,17never a substitute for loading it: when a phase names a skill, invoke it and work18from its actual guidance. Running this file as a self-contained procedure —19hand-writing blocks, authoring DA HTML from memory, skipping the browser/visual20check — is the single most common way a run goes wrong. Phase 2.3 turns the21confirmed plan into an explicit manifest of the sub-skills you must invoke.2223## Two paths2425Classify each section of the design, then follow the matching path:2627- **Content only** — every section maps to a block that **already exists** in28 the target project, or to **default content** (plain headings/paragraphs/29 images/buttons — no block). Author content and deploy. No code changes.30- **Content + code** — a section needs a block the project **does not have**,31 or an existing block matches structurally but its **styling diverges** (the32 look lives in block-specific CSS you'd have to edit). Create it as a **new,33 isolated block** (via the block-building skills), push the code, then author34 content and deploy. **Never skin an existing block or add per-section rules35 to global CSS** — new, additive blocks only. (Retargeting the project's36 global design tokens is a separate, allowed theming step; see Guardrails.)3738A single design usually mixes all three (known blocks + default content + one39or two new blocks).4041## When to use4243- The user has a Figma frame representing a page and wants it as an EDS page in DA.44 The common case: a customer **already on EDS**, with their own blocks, gets a45 new design for a new page — some sections reuse existing blocks, some need new46 ones.47- **The default path is infer-and-confirm.** Usually the frame is **not**48 annotated (e.g. the user just says "migrate this page"): the skill **infers**49 each section's mapping against the project's existing block palette and50 **confirms the plan** before building, asking whenever a section is ambiguous51 (Phase 2). This path needs nothing but the design itself.52- **Annotations are an optional accelerator — never required.** If a frame53 happens to declare each section's block / default content / new block (see54 [references/annotation-contract.md](./references/annotation-contract.md)),55 those declarations are taken as authoritative and skip the inference for that56 section. Absent them, nothing is lost — the skill infers and confirms.5758### When NOT to use5960- **Redesign / restyle an existing EDS site**, or convert arbitrary generated61 static HTML (Mobirise, Relume, v0, exported Figma HTML). Use **snowflake**.62- **Universal Editor or AEM Cloud Service (Java/OSGi/JCR).** Out of scope.6364## Related skills — orchestrated by this one6566| For | Use skill |67|---|---|68| DA IMS token (`DA_TOKEN`) | **da-auth** |69| DA body-fragment HTML rules, Source API, preview/publish, media | **da-content** |70| Whether a block exists + its authoring model & examples | **block-collection-and-party** |71| Surveying the whole available block palette | **block-inventory** |72| Designing a content model for a **new** block | **content-modeling** |73| Building a **new** block (full dev workflow) | **content-driven-development** (invokes **building-blocks**, **testing-blocks**) |74| Rendering a block + **visual comparison to the design** (the reuse gate) | **testing-blocks** (browser/Playwright screenshot + "compare implementation to design") |7576The DA-write contract in Phase 5 is the same one **da-content** documents77(see its `references/html-content.md` and `references/platform.md`).7879---8081## Inputs (gather before Phase 1; ask if missing — never guess)8283- **Figma reference** — file key + node id of the page frame (from the84 figma.com URL or the current Figma MCP selection). A file usually holds85 **many frames** — desktop/mobile variants, A/B versions, work-in-progress86 copies of the same page. Confirm **exactly which frame** to build; don't87 assume the first or largest. Two frames that are variants of the *same* page88 are one page, not two — ask which is canonical rather than deploying both.89- **Target project** — a local checkout of the EDS project repo (needed to see90 existing blocks under `blocks/`, and required for the content+code path to91 add block code). Its GitHub `{owner}`/`{repo}` and the deploy `{branch}`.92- **DA location** — `daOrg`, `daRepo` (the DA namespace), page `PATH` (no93 extension, lowercase/dash only — see da-content platform rules). In the94 standard EDS+DA setup `daOrg`/`daRepo` **equal** the GitHub `{owner}`/`{repo}`;95 confirm, because Phase 5 writes to `daOrg`/`daRepo` but previews/renders on96 the GitHub `{owner}`/`{repo}`/`{branch}`.97- **`DA_TOKEN`** — via **da-auth**, which exports `$DA_TOKEN` and caches it at98 `~/.aem/da-token.json` (valid ~1h). Prefer the `$DA_TOKEN` da-auth already set99 in this session; read the cache file only if that's unset. Two distinct100 failures: a `401` with an empty body means the token **expired** → re-auth; a101 cache file that **can't be read because the execution sandbox has no `$HOME`102 access** means the token is *unreachable*, not expired (see Phase 0 step 3) —103 don't conflate them.104105---106107## Phase 0 — Preflight (fail fast, before any read or write)108109Verify the run can actually complete **before** reading the design or writing to110DA — a missing prerequisite caught here is one actionable message; caught mid-run111it is a confusing, half-built page. Run these checks in order and, on the first112that fails, **stop with the specific remediation below** — do not proceed on a113guess or a partial capability.1141151. **Figma MCP reachable.** Confirm a Figma MCP is connected and responds via a116 cheap call (e.g. `whoami`, or listing its tools). If **no Figma MCP tool is117 available at all**, stop: *"No Figma MCP is connected. Connect one (Claude118 desktop Dev Mode, an IDE Figma integration, or a remote Figma MCP) and119 re-run."* Record the authenticated identity (`whoami`) for the next check.1202. **Access to the specific file.** Make one lightweight call against the target121 `fileKey` (e.g. `get_metadata` scoped to the frame, or `get_design_context`122 on the node). A **permission / not-found** error (`403`/`404`/"no access")123 means the file is not shared with the authenticated account → stop: *"Figma124 reports no access to `<fileKey>` as `<whoami>`. Share the file with that125 account, switch accounts, or provide a file you can open."* **Distinguish this126 from a transport cap** — a truncated, garbled, or JSON-parse-error response is127 the size cap (see Phase 1), **not** an access failure: retry narrower, do not128 report it as no access.1293. **DA write path available.** Confirm a `DA_TOKEN` is obtainable via **da-auth**130 — prefer the `$DA_TOKEN` it exports into the session, else its cache at131 `~/.aem/da-token.json`, else freshly minted. **If the cache exists but can't be132 read because this execution sandbox has no `$HOME` access**, the token is not133 missing — it is *unreachable*; do **not** loop re-minting. Stop with that134 distinction spelled out: *"A DA token exists but this sandbox can't read135 `~/.aem/da-token.json` — run where the cache is readable, or provide the token136 as `$DA_TOKEN` (or a readable path)."* If no token can be obtained at all,137 stop: *"Can't obtain a DA token (da-auth) — authenticate to DA and re-run."*138 Either way, don't spend a full Figma read only to fail at the deploy step.1394. **Project checkout + orchestrated skills present.** The target repo is checked140 out locally (needed to see `blocks/` and to add new-block code) and the skills141 this one orchestrates (**da-auth**, **da-content**, the block skills) are142 available. If the checkout path is unknown, ask for it.143144On all-pass, print a one-line preflight summary — Figma identity, the file/frame,145and the DA `org/repo` + `branch` you will write to — then proceed to Phase 1.146147---148149## Phase 1 — Read the Figma design (Figma MCP)150151Use a Figma MCP (Claude desktop / IDE / external). **Introspect the actual tool152schemas** — signatures differ between MCP implementations (the local Dev Mode153server often works off the current selection and may not take a `fileKey`; the154remote/desktop server takes `fileKey` + optional `nodeId`). The tools you need,155by capability:156157- **Structure** (e.g. `get_metadata`) — the frame's section/layer tree; node158 ids, names, positions, sizes. Derive the section list from the **content159 groups** in visual order (sort by `y`) — **not** the raw child list: full-160 bleed background rectangles, overlays, and decorative shapes are *part of* a161 section (its background), not sections of their own, and a single visual162 section is often split across sibling nodes (e.g. a background rect + a tab163 strip + a text group). Ignore the decorative layers and group the rest into164 sections by position. Usually `fileKey` required, `nodeId` optional. **Some165 MCP servers cap response size — even a single frame's structure dump can166 exceed it; scope the call to the frame or, if that still fails, one section167 at a time. A truncated, garbled, or JSON parse-error response *is* the cap168 being hit — retry narrower; do not read it as "no structure."**169- **Visual** (e.g. `get_screenshot`) — a per-section reference image to170 sanity-check the block/content mapping.171- **Content & assets** (e.g. `get_design_context`) — text, links, and image172 asset download URLs for a node. For the content+code path this also provides173 the layout/structure a new block must reproduce. **Request the lean form** —174 exclude the screenshot from the context call (fetch visuals separately with175 the screenshot tool) and disable any Code Connect lookup (e.g.176 `excludeScreenshot` / `disableCodeConnect`-style options) unless you are177 mapping to a real component library; both add payload and round-trips and can178 push a large response over the transport cap. Icons are usually **component179 instances**, not raster fills — obtain their **SVG** (export/copy as SVG),180 never a PNG, for the `/icons` or DA `/media` reference in Phase 4.181- **Design tokens** (e.g. `get_variable_defs`) — colors, spacing, type. Read182 annotation values and, for new blocks, source token values.183184**Call budget & order — Figma MCP calls are rate-limited and payload-capped, so185spend them deliberately rather than re-fetching:**1861871. **`get_metadata` first** (scoped to the frame) — the structure/section tree.188 The cheapest orienting call; every later call keys off the node ids it returns.1892. **`get_screenshot` of the whole frame early** — one full-frame reference image190 up front is the anchor you reconcile the section count against (segmentation191 heuristic) and, later, compare the rendered page to (Phase 5 Stage B). Take192 per-section crops afterwards, only for the sections you actually build.1933. **`get_design_context` targeted and lean, per section** — request the lean194 form (exclude the screenshot, disable Code Connect) and scope it to **one195 section's node at a time**. A whole-frame context dump is the single call most196 likely to blow the transport cap.1974. **Asset download last** (`download_assets` / export-as-SVG) — only for the198 assets the **confirmed** plan references, after Phase 2. Don't pull binaries199 for sections that end up reusing an existing block or being cut.200201A `429`/rate-limit or a truncated/garbled response is a **budget/cap signal, not202"no data"**: back off, narrow the scope (frame → section), and retry — never read203it as an empty design or as missing access (Phase 0 step 2 draws the same line).204205Produce an ordered **section inventory**: `{ sectionNodeId, annotation,206screenshot, content, background }` — capture each section's **background /207theme** (e.g. alternating light and dark sections), because the global token208retheme (Guardrails) recolors blocks but does **not** switch a section's209background: that carries via a `section-metadata` `Style` class or a block's210own defined dark/light variant (Phase 4). Read annotations per211[references/annotation-contract.md](./references/annotation-contract.md).212213> **Segmentation heuristic** — when the frame has no explicit grouping, derive214> the section list like this, not from the raw child order:215> 1. Sort the frame's direct children by `y` (top to bottom).216> 2. **Drop pure-decoration layers** from the section list — full-bleed217> background rectangles, gradients, blurs, absolutely-positioned shapes with218> no text or interactive child. Record each as the *background* of the219> content it sits behind (→ Phase 4 `section-metadata`); don't emit it as a220> section of its own.221> 3. **Merge siblings that form one visual band** — nodes whose vertical222> extents overlap or sit within ~one line-height of each other (a background223> rect + a heading group + a button row are *one* section, not three).224> 4. **Reconcile the count against the screenshot** before resolving: the eye225> sees the real sections; a mismatch means you over- or under-split — fix it226> before Phase 2.227228> **Placeholder content is common — don't ship it.** Designs routinely contain229> dummy copy (`Lorem ipsum`, a CTA literally labelled "Button" or "Lorem230> Ipsum", the same card title repeated across every card) and unfilled slots231> (empty or transparent image cells, blank stat boxes). Author from the **real232> text and media in the design context** — not from the placeholder and not from233> invented filler. Where it's clearly placeholder, **flag it in the plan and234> confirm the real copy/media with the user** rather than publishing "Lorem235> Ipsum" to a live page. Distinct items (cards, tabs, news entries) need236> **distinct** copy and images — repeated-identical content is itself a237> placeholder smell. If the design *itself* carries only placeholder, you cannot238> manufacture the real content: stop and get it from the user before publish.239240> Site chrome (nav bar, footer) is usually **not page body** — in EDS it is241> sourced from separate `/nav` and `/footer` documents via the header/footer242> blocks. Don't author it into the page unless the user asks.243244---245246## Phase 2 — Resolve each section247248Every section resolves to exactly one of: **existing block** (→ 3A),249**default content** (→ 3C), or **new block** (→ 3B). How that decision is250reached depends on whether the section is annotated.251252### 2.0 — Know the project's block palette (always)253254Before resolving anything, enumerate what the project **already has**:255`ls -d blocks/*/` plus **block-inventory** / **block-collection-and-party** for256each block's **authoring model** (row/cell structure, variants) **and a257rendered example** — the block's `liveExampleUrl` when it comes from the Block258Collection, or the project's own block rendered at `localhost:3000`. That259rendered example is the "block side" of the 2.1 / Phase 3A visual check. This260is the reuse-candidate set — essential when the customer is already on EDS with261their own blocks.262263### 2.1 — Resolve each section (annotation-first, else infer)264265**If the section is annotated** (see266[references/annotation-contract.md](./references/annotation-contract.md)), the267annotation is **authoritative**: named block that exists → existing block (3A);268marked `new` (or absent-and-user-confirmed) → new block (3B); plain prose/media269→ default content (3C).270271**If it is not annotated** (e.g. "just migrate this page"), **infer** the272mapping — do not dump it as unresolved:2732741. Plain prose/media (headings, paragraphs, images, a standalone link) with no275 repeating structure → **default content** (3C).2762. Otherwise match it against the 2.0 palette using the **reuse gate (structure277 AND visual, Phase 3A)**: does its content model fit an existing block *and*278 does that block's rendered example — under the project theme — look like the279 section, allowing only token differences and variants the block defines?280 - **Both fit → existing block** (3A).281 - **Structure fits but the look diverges** (bespoke card/layout/decoration282 the block's CSS can't produce without editing it), **or nothing fits →283 new block** (3B).284 - **A section carrying an interactive control** — tabs / segmented switch,285 accordion, carousel or slider, toggle — is structural divergence no static286 block reproduces: route it to a **new block** (3B), or, if the control is287 non-essential chrome, **confirm with the user** whether to keep it or288 flatten it to static content. Don't silently drop the interaction or fake289 it with a look-alike static block.2903. Attach a **confidence** to every inference: `high` (clear reuse match, or291 clearly novel) or `low` (structure fits but styling is borderline; two292 blocks plausibly fit; new-variant-vs-new-block; content model ambiguous).293294### 2.2 — Confirm the plan before deploying (never deploy a guess)295296Present a **resolution plan** — one line per section: decision (reuse `X` /297default content / new block `Y`), confidence, a one-clause rationale, and a298**content flag** on any section whose copy or media is placeholder (Phase 1)299and needs real content before publish.300301- **High-confidence sections auto-proceed through building** (Phases 3–4) —302 don't block on them.303- **Stop and ask before building** any `low`-confidence section or genuine304 ambiguity, offering the concrete choice (reuse this block vs. new block;305 which block; new variant vs. new block). Wait for the answer.306- **Pause once before deploying (Phase 5)** whenever the plan contains any307 **inferred** (unannotated) mapping: show the final plan and get a single308 confirmation before the da.live write/preview — deploy is outward-facing and309 hard to reverse. Skip this pause only if the user pre-authorized an310 unattended run. A **fully annotated** plan needs no pause — the annotations311 are the authorization.312- **Flag an existing target page.** Before confirming, check whether the target313 `content/<PATH>.html` already exists in DA (a cheap Source-API `GET`, Phase 5);314 if it does, deploying **overwrites** it — say so in the plan and get explicit315 overwrite confirmation. Never silently clobber a page you didn't create, even316 on an otherwise pre-authorized unattended run. **Record two facts per path** for317 Phase 5 to enforce: `PLANNED_STATE` (`new` if the check returned 404, `exists`318 if 200) and `OVERWRITE_OK` (`yes` only when the user confirmed overwriting an319 existing page). Phase 5 re-checks existence right before writing and **refuses**320 if the state changed since planning (a page appeared in the gap) or overwrite321 was never confirmed — the plan-time check alone is not a license to clobber.322- The user can override any line.323324Never silently drop a section, and never deploy an **inferred** mapping the325user has not seen.326327**Worked example** — an unannotated 4-section frame; this is the plan you328present in 2.2 (one line per section):329330| # | Section | Decision | Conf. | Why | Content |331|---|---|---|---|---|---|332| 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 |333| 2 | 3 feature blurbs — icon + title + text | reuse `cards` | high | content model and rendered look both fit | ok |334| 3 | Metric strip — 3 big numbers + labels | **new block** `stat-cards` | high | bespoke panel look no existing block produces (3B) | ok |335| 4 | Newsletter row — heading + email field + button | **new block** / confirm | low | carries an interactive control (input) — ask keep vs. flatten (G5) | ⚠ placeholder copy |336337Then act on it: sections 1–2 build without blocking; #3 builds (high-confidence338new block); **#4 stops for a decision** (low-confidence + interactive control);339and because the plan contains inferred mappings, the whole thing gets **one340pre-deploy confirmation** before the da.live write. Section #4's ⚠ flag means341its real copy must be supplied before publish, not shipped as placeholder.342343### 2.3 — Lock the orchestration manifest (which sub-skills this plan requires)344345Turn the confirmed plan into an explicit **manifest** of the sub-skills it346requires and **invoke each one** — this is where the intro's *orchestrate, don't347inline* rule becomes a concrete, ticked list. This file's summaries never348substitute for loading the named skill.349350Derive the manifest from the plan:351352| The plan contains… | You MUST invoke |353|---|---|354| **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). |355| 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). |356| 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. |357| **Default content** (3C) | **da-content** only (no block skills). |358359Record the manifest as an evidence-bearing checklist and tick each item **only360after you actually invoked the skill** — "I know what it does" is not invocation,361and an un-invoked required skill means this phase is **not complete**:362363- [ ] **da-content** reference docs loaded (`html-content.md` / `platform.md` / `media.md`)364- [ ] **block-collection-and-party** invoked for every reused block *(if any 3A)*365- [ ] **content-modeling** + **content-driven-development** invoked for every new block *(if any 3B)*366- [ ] **Default-content** sections authored via **da-content** alone — **no** block-building skills invoked for them *(if any 3C)*367- [ ] **testing-blocks** invoked — its browser render + visual comparison **is** the368 Stage B pre-publish check (Phase 5); a run with **no** browser available369 reports the page **preview-only, UNVERIFIED**, never "done".370371If the environment genuinely cannot run a required skill (e.g. no browser for372testing-blocks), **say so explicitly in the report and mark the affected checks373unverified** — never silently substitute this file's summary and call it passed.374375---376377## Phase 3A — Map content into an EXISTING block378379**Reuse gate — structure AND visual.** An existing block is a valid target380only when the section both (a) **fits the block's authoring model** (its381row/cell structure and field types) *and* (b) **matches the block's rendered382appearance** under the project theme, using only tokens and variants the block383already defines. Structural fit alone is **not** enough: if the section's384visual identity — bespoke layout, corner radius, shadow, decorative treatment —385lives in that block's own CSS, you cannot reproduce it without editing the386block (forbidden), so route the section to **Phase 3B** (new block). Global,387token-level differences (palette, fonts, type scale) do **not** break reuse —388they are absorbed once by retargeting the project's design tokens (see389Guardrails). **How to run the visual check — reuse testing-blocks, don't invent390one:** get a rendered example of the candidate block — its `liveExampleUrl`391(block-collection-and-party / block-inventory) or the project's own block392rendered at `localhost:3000` with the section's **actual** content — including393secondary text, captions, and CTAs over whatever background or media the block394places them on, not just placeholder cells — then follow **testing-blocks**'395browser/Playwright-MCP screenshot pass (mobile/tablet/desktop) and its "compare396implementation to design" step, comparing that screenshot against the Figma397**section screenshot** from Phase 1. Watch for treatments a block applies to398only its primary element: one that (say) whitens a heading over dark media but399leaves the supporting text and buttons at body color passes a structural check400yet renders that text illegibly — a divergence the token retheme cannot fix.401Divergence beyond what the token retheme explains ⇒ new block (or a new402variant), not reuse. This outcome is **blocking**: the section is not resolved403until its rendered look — that text included — is faithful, and the fix is a new404isolated block/variant, never an edit to the shared block. Recording the gap in405the plan and reusing the block anyway is a **plan note, not a fix** — the Phase 5406pre-publish gate treats such a box as failed.407408Once the gate passes, **invoke block-collection-and-party** to learn the block's409authoring model (its examples show the row/cell structure and variants) — read410the block from the skill, don't guess its model from its CSS source. Then pour411the Figma content into that structure:412413- **Text** → matching cells; preserve heading levels from the design.414- **Variants** → extra class tokens on the block (e.g. `cards highlight`).415 Only apply a variant the block actually defines. (Adding a *new* variant =416 modifying an existing block = Phase 3B, not 3A.)417- **Links/buttons** → a **standalone link** (the only content of its418 paragraph) auto-promotes to a button; wrap in `<strong>` for a primary419 button, `<em>` for secondary. Do not add `target="_blank"` (decoration420 handles external links). Validate the href's URL scheme and escape the421 link text/attributes before emitting — see Phase 4, *Sanitize everything422 derived from the design*. *(da-content html-content.md §8)*423- **Images** → Phase 4 (they need real URLs).424425---426427## Phase 3B — Create a NEW block (content + code)428429Only for sections Phase 2 routed here (a needed block is missing, or an existing430block's look diverges) — the **3B** case. **Guardrails (strict):**431432- Create **new, isolated block folders** only (`blocks/<new-name>/`).433- **Do NOT** skin this block by editing an existing block, `scripts.js`, or434 `head.html`, or by adding block-specific rules to global CSS — keep it435 self-contained under `blocks/<new-name>/`. *(Retargeting the project's436 global design tokens in `styles/styles.css` — the `:root` custom properties437 and base typography — is a separate, allowed project-theming step, not part438 of building this block; see Guardrails.)*439- New block **names and variant tokens** must obey EDS block-name rules440 (da-content html-content.md §3.3): lowercase alphanumeric + single hyphens,441 **no underscores, no double dashes, must not start with a digit**442 (`pricing-table` ✓, `pricing_table` / `2col` / `promo--wide` ✗). Names must443 be unique and not collide with existing blocks.444445**Build route — invoke content-driven-development (don't hand-write the block).**446Build every new block by **invoking content-driven-development** — not by writing447block JS/CSS from scratch off this file's summary. It invokes **content-modeling**448(design the authoring model from the Figma structure/tokens) then449**building-blocks** and **testing-blocks**, and produces a self-contained450`blocks/<name>/` — no source URL, no installed substrate, no page chrome, and no451global styles. That is the route that honors the 3B guardrails above, and its452testing-blocks pass is the block's Stage B verification (Phase 5). Build a **bespoke, one-off** section453the same way — it is still an ordinary isolated block, and "one-off" changes454nothing about how it is generated.455456> **Do not use snowflake here.** Snowflake converts an *already-rendered* page:457> it requires a reachable **Source URL**, **installs an overlay substrate** into458> the repo, and in block mode emits **header/footer fragments and global459> styles/tokens** — each of which violates this skill's constraints (isolated new460> block, don't touch globals, work from the **Figma frame**, not a live URL).461> Snowflake is the right tool for a *different* entry point — converting an462> existing static/rendered site — as noted under "When NOT to use".463464Use the Figma design context/tokens from Phase 1 as the source of truth for465layout and styling. New-block **CSS must target structure, not authored466classes** — inline wrappers like `<span class="…">` are stripped inside block467cells at delivery (da-content html-content.md §3.9), so a class you emit in a468cell will not survive.469470**Make the block responsive.** A Figma page frame is almost always a single471**desktop** width, but EDS pages are responsive. Author the block mobile-first472(or with explicit breakpoints) so a multi-column layout collapses to one column473on narrow viewports, and verify at mobile / tablet / desktop via474**testing-blocks** — don't ship a fixed desktop-width block. If the design475provides a **separate mobile frame**, use it to derive the breakpoint behavior476(what stacks, what hides, how type scales) — it's the *same page*, so it feeds477one responsive block, **not** a second page (see Inputs on frame variants).478479The new block's code must be **committed and pushed to the deploy branch on480GitHub and built by Code Sync** before the page can render it — see Phase 5481(content+code).482483---484485## Phase 3C — Author DEFAULT CONTENT (no block)486487For sections Phase 2 routed to default content — the **3C** case — emit standard488document elements directly inside the section `<div>` (see Phase 4 skeleton) — no489block wrapper:490491- Headings `<h1>`–`<h6>` (preserve levels), paragraphs, lists, images.492- A **standalone link** in its own `<p>` becomes a button (`<strong>`/`<em>`493 for primary/secondary) — same rule as 3A.494- Do **not** add `class`, `id`, or `style` — decoration adds them at delivery.495496*(da-content html-content.md §6)*497498---499500## Phase 4 — Generate DA body-fragment HTML (da-content)501502Emit a **body fragment** (not a full HTML document) per **da-content**. **Invoke503da-content and load its `references/html-content.md`, `platform.md`, and504`media.md` now** — the rules quoted throughout this phase are reminders to jog the505right skill, not the source of truth. Subtle authoring rules (block-cell inline-506tag normalization, media MIME/extension derivation, metadata keys) live in those507docs; authoring from this summary alone is how they get missed. Write one file per508page to `content/<PATH>.html`.509510**Mandatory skeleton** (da-content html-content.md §1–§2): wrap everything in511`<body>` with an (empty) `<header>`/`<footer>` and a `<main>`; **each section512is exactly one `<div>` directly inside `<main>`** — the `<div>` *is* the513section boundary (no `<hr>`). Do NOT emit `<!DOCTYPE>`, `<html>`, `<head>`,514`<script>`, `<style>`, `style=`, or `class=` on default content.515516```html517<body>518 <header></header>519 <main>520 <div>521 <!-- section: default content and/or a block, in visual order -->522 <h1>Heading</h1>523 <p>Intro paragraph.</p>524 <div class="block-name variant">525 <div><div>cell</div><div>cell</div></div>526 </div>527 </div>528 <div>529 <!-- next section -->530 </div>531 </main>532 <footer></footer>533</body>534```535536- **Sanitize everything derived from the design — text, attributes, links.**537 Figma text and layer names are untrusted input to the HTML you emit; treat538 them as data, never as markup:539 - **HTML-escape** every design-derived string before it lands in the document540 — `&`→`&`, `<`→`<`, `>`→`>`, and inside attribute values also541 `"`→`"` and `'`→`'`. A heading `Tips & Tricks <Beta>` must serialize542 as `Tips & Tricks <Beta>`, never as raw markup that can break the543 document or inject an element.544 - **Validate every link's URL scheme** against an allowlist — `http`, `https`,545 `mailto`, `tel`, or a root-relative (`/…`) path. **Reject `javascript:`,546 `data:`, `vbscript:`, and any other scheme** (a prototype link can carry547 anything): drop the href or ask the user — never emit it.548 - **Admit a Figma-derived class token only after block-name validation** — a549 layer/frame name becomes a block or variant class *only* once it passes the550 EDS name rules in Phase 3B (lowercase alphanumeric + single hyphens, no551 underscores/double-dashes, not digit-initial); never pass a raw layer name552 through as a class.553- **Blocks — canonical div form:** `<div class="block-name variant">`, each554 direct child `<div>` a row, each grandchild `<div>` a cell. The first class555 token is the block name (resolves to `blocks/<name>/<name>.{js,css}`).556 Multi-word variants hyphenate; multiple variants are separate class tokens.557 Max 4 cells per row; blocks cannot nest. *(html-content.md §3)*558- **Default content:** headings/paragraphs/lists/images/buttons live directly559 in the section `<div>`, outside any block. *(html-content.md §6)*560- **Icons — two non-interchangeable paths; never a stand-in glyph.** The561 `<span class="icon icon-<name>"></span>` convention resolves **only** to the562 project's Code Bus `/icons/<name>.svg`, so that SVG must be **committed to the563 repo `/icons/` folder and pushed on the deploy branch** (content+code path,564 same as block code) and return `200` on the branch host — uploading it to DA565 `/media` does **not** satisfy the span (it 404s and the icon silently vanishes).566 A DA-`/media` SVG must instead be referenced by **full URL on an `<img>`**, not567 an icon span. Get the real **SVG** in Phase 1; **never emit an emoji or Unicode568 glyph in place of a designed icon.** *(html-content.md §7)*569- **Images — MUST be full, fetchable URLs.** Figma render URLs expire, and570 **repo-relative paths (`/img/…`) render as `about:error`.** So: download the571 image bytes from Figma (Phase 1 asset URLs), **upload each binary to DA**572 (`PUT admin.da.live/source/{daOrg}/{daRepo}/<media-path>`), and reference573 `https://content.da.live/{daOrg}/{daRepo}/<media-path>`. External image URLs574 are also accepted (the preview sideloads them). Author a bare `<img alt="…">`575 and let the pipeline build the `<picture>`.576 - **Normalize format, extension, and MIME together — from the bytes, never the577 URL suffix.** Detect the real format from the image's magic bytes (or the578 asset's reported `format`), then make **all three agree**: the multipart579 `type=` MIME, the `<media-path>` file extension you PUT to, and the extension580 in the `content.da.live` URL you author. Design tools routinely export JPEG581 bytes under a `.png`-named asset; trusting the suffix gives you a `.png` path582 served as `image/jpeg` (or the reverse) — a latent corruption bug. A layer583 that *looks* vector (an icon, a logo, a shape) often comes back **rasterized**584 — `download_assets` returns it under `rawImages` with `svgAssets` empty — so a585 design that implies `.svg` can hand you PNG/JPEG bytes. Author each `<img>`586 extension from the bytes you actually downloaded, never from the layer's587 apparent type or name. Canonical588 mapping: JPEG→`.jpg`/`image/jpeg`, PNG→`.png`/`image/png`, WebP→`.webp`/589 `image/webp`, GIF→`.gif`/`image/gif`, SVG→`.svg`/`image/svg+xml`. If bytes590 and asset-reported format disagree, trust the bytes.591 *(html-content.md §9 + media.md)*592- **Section styling** → a `section-metadata` block **inside** the section593 (`Style` → CSS classes; other rows → `data-*`). *(html-content.md §4)*594- **Page metadata** → a single `metadata` block (exact class), placed as the595 **last element of the last section inside `<main>`** (never after `</main>`596 or in `<footer>`); keys like `title`, `description`, `image`, `template`,597 `theme`. **Author it from the design — don't leave it empty or a bare598 comment.** Derive `title` from the frame name or the page `<h1>`, `description`599 from the hero/intro copy (a concise real sentence, never lorem), and `image`600 from the primary/hero image's uploaded DA URL when the design has one. This601 block is **required** — the Phase 5 pre-publish gate blocks on its absence — so602 populate it rather than deferring it. If the design offers no usable603 title/description text, ask the user rather than inventing marketing copy.604 *(html-content.md §5)*605606Inside block cells the pipeline runs a stricter inline-tag normalization than607for default content — `<span class>` is unwrapped (class lost), `<b>`→`<strong>`,608`<mark>`→`<em>`, etc. Restrict cell content to the html-content.md §3.9 preserve609list. A wrong metadata **key** or block **field** silently corrupts output;610when unsure, read da-content.611612---613614## Phase 5 — Deploy to DA615616**If a DA MCP server is available in the session, use its tools** for auth and617source writes (da-auth and da-content both defer to it when present).618Otherwise use the Source API directly, below.619620```bash621# Two identities — keep them separate. DA (Document Authoring) and GitHub are the622# same org/repo in the standard EDS setup, but nothing guarantees it, so never623# assume one from the other. DA endpoints (admin.da.live/source, content.da.live,624# da.live/edit) use the DA pair; the render host and admin.hlx.page (code, preview,625# live) use the GitHub pair.626DA_ORG=<da-org> # Document Authoring org627DA_REPO=<da-repo> # Document Authoring repo/site628GH_OWNER=<gh-owner> # GitHub owner629GH_REPO=<gh-repo> # GitHub repo630# In the standard setup all four match: DA_ORG=GH_OWNER=<owner>, DA_REPO=GH_REPO=<repo>.631BRANCH=<branch> # git deploy ref (usually main). For content+code this MUST be632 # the branch the new-block code was pushed to and Code Sync built.633BRANCH_HOST=${BRANCH//\//-} # host label: slashes → dashes ('feature/x' → 'feature-x').634 # Used BOTH for the aem.page/aem.live hostname AND as the ref635 # segment in every admin.hlx.page path (code/preview/live): that636 # ref is a SINGLE path segment, so a slashed branch ('figma/x')637 # splits it and 404s — pass the dashed label ('figma-x'), which is638 # what AEM actually resolves. Only git itself (push/checkout) uses639 # the literal slashed $BRANCH. For a slash-free branch the two640 # forms are identical, so $BRANCH_HOST is always the safe choice641 # for admin.hlx.page.642P=<path-without-extension>643TOKEN="$DA_TOKEN" # from da-auth; 401 w/ empty body ⇒ expired, re-auth644645# Fail fast if the branch host would be unresolvable (>63 chars won't resolve).646host="$BRANCH_HOST--$GH_REPO--$GH_OWNER"647[ "${#host}" -le 63 ] || { echo "❌ branch host '$host' is ${#host} chars (>63) — won't resolve; use a shorter branch/repo/org"; exit 1; }648649# --- checked-request helper: every call asserts its status; a bare `curl -sS`650# exits 0 on 401/403/409/5xx, so an unchecked curl silently "succeeds" on a651# failed write. req <expected-codes> <curl-args…>: prints the body, retries a652# few times on network/429/5xx, and aborts (non-zero) on any other mismatch.653# Use it for every PUT/POST below; if a DA MCP server is used instead, apply654# the same rule — assert the returned status, don't assume success. ---655req() {656 local expect="$1"; shift657 local attempt out code body658 for attempt in 1 2 3 4 5; do659 if out=$(curl -sS -w $'\n%{http_code}' "$@"); then code="${out##*$'\n'}"; else code="000"; fi660 body="${out%$'\n'*}"661 case ",$expect," in *",$code,"*) printf '%s' "$body"; return 0;; esac662 case "$code" in663 000|429|5??) sleep $((attempt * 2)); continue;; # transient — bounded retry664 401) echo "❌ 401 (empty body ⇒ token expired) — re-auth (da-auth) and retry" >&2; return 1;;665 *) echo "❌ HTTP $code (expected $expect) — $*" >&2; return 1;; # 4xx: do not retry666 esac667 done668 echo "❌ giving up after retries (last status $code) — $*" >&2; return 1669}670671# --- content+code path ONLY: block code must be LIVE before the pag672673…(truncated)