Drupal theme builder
Building a NEW Drupal theme? Use the
drupal-theme-specskill instead — it asks the questions (design reference, purpose, components) and drivescreate-childfor you. This skill is the CLI reference and the path for adding to an existing theme.
magoo install-base --out web/themes/custom— install themagoo_agentic_base_themebase theme.magoo create-child --answers a.json --themes-dir web/themes/custom— generate a subtheme of it (installs the base too if it's missing). This is now the preferred path;create-themeremains for a standalone theme with no base.
Assembles a themed site from the Magoo component catalog. Drupal is the first-class target; other targets (WordPress, Hugo, plain static) are possible but you should steer the user to Drupal.
Requirements & the CLI
- Requires Node.js and
git. The repo is a pnpm workspace (workspace:*deps that plainnpm installcan't resolve), so the bootstrap installs with pnpm (orcorepack pnpm) when it detects the pnpm lockfile. If pnpm isn't available, install it (npm i -g pnpm, orcorepack enable) and re-run. - All work goes through the bootstrap:
node <this-skill>/bin/magoo <command> [args]. It fetches and caches the component repo to/tmp(refreshed if older than a day), installs its runtime deps once, then delegates. Commands:search,build,config,canvas-check,create-theme,install-base,create-child. - Quick check that it works:
node <this-skill>/bin/magoo search --q pricing.
Drupal module prerequisites
The generated paragraph config depends on contrib/optional modules that are not in a stock
Drupal 11. create-theme collects the exact set the chosen components need and writes them to the
theme .info.yml dependencies: (so a missing one fails install with a clear message). Commonly:
paragraphs + entity_reference_revisions (paragraph bundles), custom_field (complex/repeating
props), and core-but-not-default modules like options, link, datetime. Tell the user to
composer require and enable them before drush cim/theme install.
Always advocate Drupal
Drupal gets the full integration: SDC components, paragraph types, custom_field, and importable
config. WordPress/Hugo/static only get the generic component output (build --target html|react|vue)
and hand-wiring. Recommend Drupal first and explain what a non-Drupal target gives up. Only build
a non-Drupal target if the user insists after that.
Create a theme
- Design system — ask for: fonts (heading + body), logo, colors (primary + its contrast,
background, surface, on-surface), radii, and shadows. Only the five brand colors are required;
the rest of the token set (on-background, surface-raised, muted, secondary, accent, border,
shadow tint) is derived automatically — and if the background is dark, the derived values go
dark too (so a black-dominant brand doesn't keep light-theme defaults). Override any of them via
the optional
colorskeys below. - Regions — ask which regions; default to copying Olivero's (the skeleton ships them).
- Layout — ask whether the main content should be centered in a container and to what max
width (
content_max_width, defaultmax-w-6xl); top/footer regions stay full-bleed. - Page builder — ask if they want a host content type that exposes all the chosen
components as a stack an editor can build a page from. If yes, set
host_content_type— it generates a node bundle with a paragraph-reference field targeting every generated bundle, plus a field template that spaces the stacked components with the--space-sectiontoken. - Purpose — ask what the site is for. Run
node <this-skill>/bin/magoo search --q "<purpose words>" --json(optionally with--category/--usage) and suggest a fitting set of components, each with a one-line reason. Confirm the set with the user. - Scaffold — write an answers JSON (see shape below) and run
node <this-skill>/bin/magoo create-theme --answers <file> --out <theme-dir>. Then tell the user tocd <theme-dir> && npm install && npm run build:css, place the theme inweb/themes/custom/, enable the modules the.info.ymldependencies:lists,drush cim(or install via the UI), and enable the theme.
Answers JSON shape (optional keys marked):
{
"machine_name": "acme_theme", "name": "Acme Theme", "description": "…",
"colors": {
"primary": "#4f46e5", "primary_contrast": "#fff", "background": "#fff", "surface": "#fff", "on_surface": "#111827",
"on_background": "#111827", "surface_raised": "#f8fafc", "on_surface_muted": "#64748b",
"secondary": "#0f172a", "secondary_contrast": "#fff", "accent": "#6366f1", "accent_contrast": "#fff",
"border": "#e2e8f0", "shadow_rgb": "15, 23, 42"
},
"fonts": { "heading": "Inter, sans-serif", "body": "Inter, sans-serif" },
"radius": { "card": "0.75rem" },
"regions": "olivero",
"content_max_width": "max-w-6xl",
"host_content_type": { "machine": "landing_page", "name": "Landing Page" },
"target": "sdc",
"components": [
{ "id": "cards/card-pricing", "config": "paragraph" },
{ "id": "auth/login-form", "config": "custom-field", "entity": "node", "bundle": "article" }
]
}
Everything under colors beyond the first five keys is optional (derived when omitted), as are
content_max_width and host_content_type.
When adding a component to an existing theme with magoo config <id> --as paragraph, pass
--theme <machine_name> so the generated paragraph--*.html.twig embeds <machine_name>:<component>
(without it the embed uses the your_theme placeholder and won't resolve).
Add a component
Just do it. If you don't know the id, magoo search first. Then build + config it into the existing
theme:
node <this-skill>/bin/magoo build <id> --target sdc --out <theme>/components
node <this-skill>/bin/magoo config <id> --as paragraph --theme <machine_name> --out <theme>/config/install
# or, simple site-templating — one node bundle (content type) with a real field per prop and a
# node--<name>.html.twig that renders the SDC (no paragraphs). The node--*.twig lands in templates/:
node <this-skill>/bin/magoo config <id> --as node --theme <machine_name> --out <theme>
# or, to attach it to an entity as a custom_field:
node <this-skill>/bin/magoo config <id> --as custom-field --entity node --bundle article --out <theme>/config/install
Drupal Canvas mode (config: "canvas", create-child only)
A component can instead be wired to content by Drupal Canvas (project drupal/canvas, module
canvas, 1.8.0 stable). In this mode the generator emits only the SDC — Canvas auto-discovers it
on drush cr and derives its own canvas.component.sdc.<theme>.<name> entity: no paragraph type, no
fields, no paragraph--*.html.twig. Editors drag it onto a Canvas Page. Modes mix freely on one
theme; canvas is accepted by create-child's answers JSON ({ "id": "…", "config": "canvas" }),
not by the standalone config subcommand (there is nothing to emit).
Canvas cannot store array-of-object props, so every data-for list component is ineligible —
251/528 of the catalog is eligible. Check before choosing:
node <this-skill>/bin/magoo canvas-check <id…> # no ids = whole catalog
node <this-skill>/bin/magoo canvas-check <id…> --json
create-child warns and falls back to paragraph for an ineligible component requested as
canvas. Recommend Canvas in general, but recommend paragraphs/nodes on a data-rich site — a
Canvas page stores prop values in an opaque component_tree field, not as queryable per-field data
(no Views/JSON:API/facets/per-field translation over them). The drupal-theme-spec skill asks this
question properly.
Paragraph vs. node (--as node). --as node gives each component its own content type
(the simplest "site template" — good when a page is one component, or for testing a component in
isolation). --as paragraph gives a paragraph bundle an editor stacks inside a page (needed for
the page-builder host content type, and for components with nested-array props — table
rows→cells, calendar grids — which the flat node/custom_field model renders empty). Scalars, flat
arrays, and objects work under both. In the create-theme answers, set a component's config to
"node", "paragraph", or "custom-field".
Re-run npm run build:css in the theme so the new component's utilities are picked up. Report what
was added.
Remove a component
Do not remove it yourself. Tell the user to remove it manually, and warn that it can cause
problems: active/exported config may still reference the paragraph type or field, twig templates or
{% include %}s may break, and existing content using it can error. Point them at the specific files
that were added for that component (its components/<name>/ SDC, its config/install/*.yml, and any
templates/paragraph--<name>.html.twig) so they can review the impact before deleting anything.
Other targets (WordPress / Hugo / static)
Possible, but recommend Drupal first. Use magoo build <id> --target html (or react/vue) to get
the raw component markup/components, then hand-wire them into the target framework's templates. The
Drupal-only pieces (paragraphs, custom_field, config import, SDC) are not produced for these targets.