Search by

drupal / atelier

Drupal-Infrastructure

A Drupal CMS site template whose entire front end is Drupal Canvas code components: twenty-one components and ten pages, styled by their own Tailwind build rather than by a theme.

Package info

git.drupalcode.org/project/atelier.git

Type:drupal-recipe

pkg:composer/drupal/atelier

Statistics

Installs: 25

Dependents: 0

Suggesters: 0

1.0.x-dev 2026-11-12 22:38 UTC

This package is auto-updated.

Last update: 2026-11-12 23:17:35 UTC


README

A Drupal CMS site template that uses Drupal Canvas code components.

There is no front-end theme in the usual sense. The theme is a shell that emits regions and no CSS. The output comes from the components and their Tailwind build.

The codebase is a React and Tailwind project generated by npx @drupal-canvas/create, which pushes components to a site over the Canvas CLI.

Install

Require the recipe inside a CMS project, then pick "Atelier" in the installer:

composer require drupal/atelier

Instead of the installer UI, from the command line:

drush site:install recipes/atelier

What the template configures

A Drupal CMS site. The template is built on drupal_cms_site_template_base, so you get the whole Drupal CMS admin experience..

A site template should be built on that base rather than on another site template. Applying it also covers core/recipes/administrator_role, content_editor_role and the media-type recipes, so this template does not repeat them.

Canvas and code components. Canvas comes from the base. Code components are canvas.js_component config entities that live in the canvas module — there is no separate submodule to install. This template grants content_editor the administer code components and administer page template permissions, so editors can work with what the CLI pushes.

Authentication for the CLI. canvas_oauth, simple_oauth and consumers, with all eleven canvas:* OAuth scopes imported, including canvas:media:image:create. That last one matters: the CLI asks for it in its default scope string, and without it every push fails with invalid_scope.

A blank shell theme — no Drupal design system. atelier_theme is generated at composer install by drupal/site_template_helper, from the extra.drupal-site-template block in composer.json, with from: false. That writes a theme consisting of nothing but an .info.yml: no templates, no CSS, no SDCs.

This is the same mechanism, and the same three regions, that drupal_cms_blank uses for its blank theme — there is no separate theme project to depend on in either case, since the plugin generates it at install time. Two deliberate differences from blank: this template strips Drupal's system/base CSS per file (see the notes), and it does not disable asset aggregation or switch on theme development mode. Blank does both because you are expected to build a theme on top of it; here the front end arrives as pushed components, so neither helps.

That is the point. On a code-components site every pixel comes from the components you push, and their Tailwind build styles the whole page. Installing a Drupal-side design system such as Mercury alongside it puts two front-end systems on one page, and they fight: Mercury's CSS visibly breaks the layout of this template's own header component.

The theme declares three regions — header, content, footer — which are what regions/*.json in a paired codebase map onto. Two details there are load-bearing:

  • regions: replaces the default region set rather than merging with it, so every region you want must be listed. content is not optional: BlockPageVariant hardcodes it as the fallback for main page content and status messages, and omitting it makes both silently disappear.
  • libraries-override: {core/normalize: false} does nothing under from: false, which is worth knowing because drupal_cms_blank carries that line. core/normalize is attached only by a theme's own libraries: list, and a theme generated with from: false has none, so there is nothing to remove. It would take effect if from were a starterkit. The line that actually removes Drupal's CSS is the system/base override below.
  • libraries-override on system/base is what removes Drupal's own CSS, but it is applied per file, not to the whole library. system/base bundles five files and one of them, hidden.module.css, defines .visually-hidden — which core's own skip link depends on. Dropping the library wholesale leaves "Skip to main content" rendering as visible text at the top of every page. The other four are pure styling and are dropped. libraries-remove is not a real key, and a per-file override silently no-ops if the path does not match system.libraries.yml exactly: info.yml has no allowlist and no schema, so neither mistake reports an error.

The Atelier components, vendored as config. All 21 of them, one file each in config/canvas.js_component.*.yml, plus the header and footer page regions and the Tailwind build they are styled with. A freshly installed site therefore has a working, styled front end before anyone runs canvas push — it is not a backend waiting to be filled in.

That snapshot is generated, not hand-written: tests/regenerate-components.sh re-exports it from a site you have pushed to. Run it whenever the paired codebase changes, then review the diff.

The global CSS is applied as a setProperties config action rather than a shipped config file. canvas.asset_library.global already exists by the time this recipe's config is imported — the base recipe creates it when it installs Canvas — and a non-strict recipe only imports config that does not exist yet. @see \Drupal\Core\Recipe\ConfigConfigurator::getConfigStorage()

Setting strict to a list looks like a way around that and is a trap: a non-empty list is truthy, so core returns the entire recipe storage and overwrites every pre-existing config the recipe touches, not just the listed names.

canvas push replaces all of it with whatever is in your codebase.

Pages, so the site is not empty. Ten Canvas pages -- home, studio, services and one per service, the journal index, questions, contact and privacy -- all composed from the shipped components:

  • Home at /home, set as the front page — hero, a three-up of what the studio does, a row of work with captions, a logo row, the journal index pulled live over JSON:API, a testimonial, and a closing call to action.
  • The studio at /studio — prose sharing a two-column band with a timber detail, a numbered three-step process, and a pull quote.
  • Journal at /journal — the article_teasers component on its own, fetching articles over JSON:API in the browser.
  • Contact at /contact — a three-up of practical detail, and a call to action.

The components

ComponentWhat it is
heroThe opening statement, with an optional image beside it on a wide screen and above it on a phone. Renders the page's h1.
sectionEvery band on a page. Owns the padding above and below it, the gap between the components stacked inside it, its ground (paper, wash, or an inverted ink), and an optional rule along the top.
section_introThe eyebrow, heading, and lede that name a band. Takes a heading level.
gridTwo, three, or four columns on a wide screen, one on a phone.
featureOne idea: an optional marker, a heading, and text. Takes a heading level.
figureA drawing or photograph with a title and caption, optionally linked. Takes a heading level.
post_metaThe byline, date and reading time under a journal title. Formats the timestamp Drupal hands it, and counts the reading time from the body so it cannot drift from the words above it.
proseLong-form rich text, styled against the same tokens as everything else.
statA figure and what it measures. The value is a string, so "90 a year" works.
testimonialA quotation, attributed. Quotation marks come from the page language.
ctaA closing ask, with a slot for buttons. Brings no ground of its own.
logo_wallA labelled row of marks.
logoOne mark, optionally linked.
badgeA short label, plain or in a ruled box.
buttonSolid, outline, or text link, at three sizes. Renders an anchor with a URL and a button without one.
accordionA ruled stack of disclosures. Draws the top rule and closes the gap Section puts between its children, so a run of questions reads as one object.
disclosureOne question opening onto its answer, built on <details> so it works from the keyboard, announces its own state, and functions before the island hydrates.
article_teasersThe newest articles, fetched from JSON:API in the browser. Loading, error, and empty states are all distinct.
site_headerWordmark plus a slot, on a hairline. Emits a div; the theme region is already the banner landmark.
site_navA Drupal menu, fetched from /jsonapi/menu_items/{menu}. Renders two levels, and marks the current page.
site_footerWordmark, a credit line, and a slot.

Two of them prove the argument for code components over a static library: site_nav and article_teasers read live Drupal data in the browser, so an editor publishing an article or reordering a menu changes the front end with no rebuild and no deploy.

What the backend has to provide for those two

ComponentWhat it needs, and what this template provides
site_navjsonapi_menu_items exposing /jsonapi/menu_items/main, and a main menu with links. Both provided, and every shipped link resolves to a page this recipe also ships.
article_teasersAn article node type readable at /jsonapi/node/article. Drupal CMS ships a page type, not an article one, so this template adds core/recipes/article_content_type.

search_api, search_api_db, and jsonapi_search_api are installed with a cms_content index over nodes and Canvas pages. No shipped component queries it: it is there so that a search component you write has an endpoint to query without another round of backend setup. Drop them from recipe.yml if you would rather not carry them.

What the template deliberately does not do

It does not generate the OAuth key pair or create the consumer.

A recipe is declarative configuration. It cannot generate key material, and shipping a fixed client secret in a public site template would mean every site installed from it shares the same credentials. So the recipe stops at the declarative boundary: modules installed, scopes defined, permissions granted.

Canvas ships an interactive Drush command for the generated-secret part. Check whether your version has it:

drush list | grep canvas

If it is not there yet, the manual equivalent is below. Both produce the same result.

Finish the OAuth setup

Generate a key pair outside the document root and point simple_oauth at it:

drush simple-oauth:generate-keys ../keys
drush config:set simple_oauth.settings public_key ../keys/public.key
drush config:set simple_oauth.settings private_key ../keys/private.key

Create a consumer at /admin/config/services/consumer/add:

  • Client ID — cli matches the value a scaffolded codebase's .env.example ships, so using it means one less thing to edit.
  • New secret — anything; you will paste it into .env.
  • Is Confidential? — checked.
  • Grant types — Client credentials.
  • User — a real user, not anonymous. Canvas access checkers read this user. Note that the user's own role permissions are ignored; the scopes decide what the client can do.
  • Scopes — all eleven canvas:* scopes.

Verify before moving on:

curl -X POST https://example.ddev.site/oauth/token \
  -d 'grant_type=client_credentials&client_id=cli&client_secret=YOUR_SECRET' \
  --data-urlencode 'scope=canvas:js_component canvas:asset_library canvas:media:image:create canvas:media:view canvas:page:create canvas:page:read canvas:page:edit canvas:content_template canvas:page_region'

That scope string is exactly what the CLI requests when a codebase has sync.pages, sync.regions and sync.contentTemplates all enabled, which is the scaffold default. A token in the response means the Drupal side is ready.

Scaffold a codebase and connect it

npx @drupal-canvas/create my-site --site-url https://example.ddev.site
cd my-site

The scaffolder writes .env with CANVAS_SITE_URL already filled in. Two things to know about that value:

  • A https://*.ddev.site URL is rewritten to http://, because Node does not trust mkcert's local certificate authority. This is expected.
  • It must be the site root, not the JSON:API endpoint.

Add the credentials from the previous step:

CANVAS_CLIENT_ID=cli
CANVAS_CLIENT_SECRET=your-secret

Change a component

The twenty-one components are already on the site, so the first move is to pull them into a codebase rather than to push something new:

npx @drupal-canvas/create my-studio
cd my-studio
npx canvas pull

That writes each component to src/components/<name>/ as index.jsx plus a component.yml describing its props and slots, and the design tokens to src/global.css. Edit, preview locally with npm run dev, then:

npx canvas push

A push replaces the vendored snapshot with whatever is in the codebase. Push only what you mean to: --no-pages --no-regions leaves the shipped pages and the header and footer alone while you iterate on components.

Four things the CLI rejects, all of which cost time to discover:

  • component.yml needs machineName, status: true, and required as a top-level list — not nested under props.
  • Prop titles must match the prop name in camel case.
  • Importing from @/lib/utils is forbidden. Import cn from drupal-canvas, which exports the same helper.
  • Props typed as an array of objects are unsupported. Model repeated content as a slot and let editors compose it.

Two things the components themselves have to get right:

  • A slot arrives as a prop named after the slot, not as children.
  • <ul>/<li> around a slot does not survive: the parser hoists the slot wrapper out of the list. Use divs.

Everything else — clsx, class-variance-authority, tailwind-merge, SWR — is provided by Canvas as an external, so it does not go into your bundle.

There is no post-install message: recipe.yml has nine allowed top-level keys and none of them emits one, and a site template cannot prompt during install either. The surfaces that do exist are extra.drupal_cms_installer.links, which puts the Canvas project page on the installer card, and the landing page itself. This README is the real handoff.

Local preview without Drupal

npm run dev

The Canvas Workbench renders components locally against mocks.json, so you do not need this site for day-to-day component work — only to push.

License

GPL-2.0-or-later, like the Drupal configuration it ships. A paired codebase template it pairs with is MIT.