Problem
BlockJsonGenerator::icon() has exactly two branches:
$existing = $existingBlockJson['icon'] ?? null;
if (is_string($existing) && '' !== $existing) {
return $existing; // preserve from disk
}
return $this->loadIcon(); // packaged schemas/block-icon.svg
Preservation-from-disk only fires when a block.json is already there. Whenever it is not — a fresh clone that generates before the file exists, or anyone deleting the generated files and regenerating — every block silently gets the packaged icon instead of the project's own.
The method's own docblock names the consequence:
Without this, fields-generate --root silently rewrites every block's icon to the packaged one on the first run in a project whose brand differs — a 38-file brand regression buried in an otherwise-mechanical normalisation diff.
That guard works, but only while the files are on disk. This is the same structural hole that #18 closed for example: null: a value the generator cannot derive, protected only by a path that does not always run.
$iconPath already exists as a constructor parameter (BlockJsonGenerator:36) and no CLI ever sets it, so in practice only the two branches above are reachable.
Why per-component capture is the wrong fix
icon is deliberately absent from BlockResidualCapturer::CONFIG_SECTIONS, and that is correct. Measured in the reference project: 42 blocks, 1 unique icon, ~2 KB each. Capturing it would duplicate the same SVG into 42 definition files. Unlike example: null — a per-component decision nothing else records — the icon is one project-wide brand asset.
Proposed fix: a project-level icon path
Wire the existing $iconPath through fields-generate, resolved in this order:
- an explicit CLI flag (
--icon=<path>), for scripted or non-standard layouts;
- a conventional project path, if the file exists;
- the packaged
schemas/block-icon.svg as today's cold-start default.
Only step 2 needs a decision — which conventional path. See below, because the obvious candidate does not work.
The obvious candidate does not work — do not use favicon.svg
wordpress/gutenberg.md § Icon currently tells authors to derive the block icon from the project favicon:
If every block's icon ever needs regenerating from the canonical ${STATIC_PATH}/images/touch/favicon.svg (logo refresh / brand change): strip inkscape / sodipodi / svg namespace declarations + the <sodipodi:namedview> element, replace hardcoded fills (#2d2e36) with currentColor, then compact to one line.
Measured against the reference project, that procedure does not apply to the file it names:
|
static/images/touch/favicon.svg |
the icon actually embedded in block.json |
| size |
26 043 B |
1 999 B |
| content |
a single <image> with a base64 PNG payload |
real <path> vector artwork |
fill attributes |
none |
fill="currentColor" |
inkscape / sodipodi |
none |
— |
The favicon is a raster wrapped in an SVG envelope. There are no fills to replace and no namespaces to strip, and a base64 PNG cannot take currentColor — so it can never follow the editor's light/dark theme the way the current icon does. Deriving from it would produce a 26 KB blob that renders wrong.
So the embedded icon is a separate, hand-made vector asset that exists nowhere on disk except inside the 42 generated block.json files.
Recommendation: the conventional path should point at a dedicated vector icon — something like ${STATIC_PATH}/images/blocks/block-icon.svg — and adopting this means extracting the current artwork out of block.json into that file once. Do not reach for the favicon.
Secondary: gutenberg.md's icon procedure should be corrected. It describes transforming a file shape (Inkscape-authored vector with hardcoded fills) that at least one real project's favicon does not have, and following it literally would break the icon. That is a tailwind-base doctrine change and belongs in its own chat-first PR.
Validation worth adding
Whatever path is chosen, the generator should reject or warn on an icon that cannot work:
- contains
<image or a data:image/ payload → a raster cannot be recoloured by currentColor;
- carries hardcoded
fill values other than currentColor → will not follow the editor theme.
Both are cheap string checks and both catch precisely the mistake this issue is about.
Not urgent
Today's protection holds because the generated files are on disk in every project. This is a trap for a fresh clone, not a live defect — but it is the kind that surfaces as a 42-file diff nobody reads closely.
Related: #18 (the same structural hole, closed for example: null).
Problem
BlockJsonGenerator::icon()has exactly two branches:Preservation-from-disk only fires when a
block.jsonis already there. Whenever it is not — a fresh clone that generates before the file exists, or anyone deleting the generated files and regenerating — every block silently gets the packaged icon instead of the project's own.The method's own docblock names the consequence:
That guard works, but only while the files are on disk. This is the same structural hole that #18 closed for
example: null: a value the generator cannot derive, protected only by a path that does not always run.$iconPathalready exists as a constructor parameter (BlockJsonGenerator:36) and no CLI ever sets it, so in practice only the two branches above are reachable.Why per-component capture is the wrong fix
iconis deliberately absent fromBlockResidualCapturer::CONFIG_SECTIONS, and that is correct. Measured in the reference project: 42 blocks, 1 unique icon, ~2 KB each. Capturing it would duplicate the same SVG into 42 definition files. Unlikeexample: null— a per-component decision nothing else records — the icon is one project-wide brand asset.Proposed fix: a project-level icon path
Wire the existing
$iconPaththroughfields-generate, resolved in this order:--icon=<path>), for scripted or non-standard layouts;schemas/block-icon.svgas today's cold-start default.Only step 2 needs a decision — which conventional path. See below, because the obvious candidate does not work.
The obvious candidate does not work — do not use
favicon.svgwordpress/gutenberg.md§ Icon currently tells authors to derive the block icon from the project favicon:Measured against the reference project, that procedure does not apply to the file it names:
static/images/touch/favicon.svgblock.json<image>with a base64 PNG payload<path>vector artworkfillattributesfill="currentColor"inkscape/sodipodiThe favicon is a raster wrapped in an SVG envelope. There are no fills to replace and no namespaces to strip, and a base64 PNG cannot take
currentColor— so it can never follow the editor's light/dark theme the way the current icon does. Deriving from it would produce a 26 KB blob that renders wrong.So the embedded icon is a separate, hand-made vector asset that exists nowhere on disk except inside the 42 generated
block.jsonfiles.Recommendation: the conventional path should point at a dedicated vector icon — something like
${STATIC_PATH}/images/blocks/block-icon.svg— and adopting this means extracting the current artwork out ofblock.jsoninto that file once. Do not reach for the favicon.Secondary:
gutenberg.md's icon procedure should be corrected. It describes transforming a file shape (Inkscape-authored vector with hardcoded fills) that at least one real project's favicon does not have, and following it literally would break the icon. That is atailwind-basedoctrine change and belongs in its own chat-first PR.Validation worth adding
Whatever path is chosen, the generator should reject or warn on an icon that cannot work:
<imageor adata:image/payload → a raster cannot be recoloured bycurrentColor;fillvalues other thancurrentColor→ will not follow the editor theme.Both are cheap string checks and both catch precisely the mistake this issue is about.
Not urgent
Today's protection holds because the generated files are on disk in every project. This is a trap for a fresh clone, not a live defect — but it is the kind that surfaces as a 42-file diff nobody reads closely.
Related: #18 (the same structural hole, closed for
example: null).