Drupal Recipes

berdir.github.io/recipes/

About me

  • Sascha Grossenbacher
  • MD Systems
  • https://drupal.org/u/berdir
  • Entity system maintainer, Contrib maintainer

Topics

  • Overview
  • Abilities of recipes
  • Using recipes
  • Creating recipes
  • Limitations, Caveats, Learnings
  • Drupal CMS & recipes
  • Paragraphs & recipes

Resources

Disclaimer

This is a honest assessment and comparison of the Recipe API based on my experience converting Paragraphs Demo and maintaining our distributions at MD Systems.

I know very well how hard it so build something new like this, it is not my intention to discredit any work done on this.

Overview

The problem: Distributions

  • Difficult to update, 100% dependant
  • Can not be added to existing projects
  • Can't be removed (easily)
  • Can't combine multiple distributions

Proposed solution: Recipes

  • Instructions on how to configure a Drupal site
  • Applied to an existing site
  • Lightweight alternative to modules
  • No lifecycle: Recipes are applied, not installed
  • Declarative, not functional: YAML, not PHP

Status

Abilities of recipes

Dependencies

  • Recipes can depend on modules and other recipes
  • Limited discovery
    • Same folder + core for recipes
    • No "subrecipes"
    • No recipes within module projects

Recipe configuration

Essentially like a module, a recipe can provide any number of configuration entities.

Default content

  • Based on the format and API of the default_content project.
  • Any Content entity can be exported into a YAML format.
  • Use cases: Example content for distributions, Initial content for custom projects
  • Note: Exporter is not yet in core, use default_content drush commands

Config actions

  • Make changes to existing configuration
  • Examples: Add components to form/view displays, enable content moderation, add to text formats
  • This is the replacement to hook_install() in modules

Inputs

Recipes can define required input and use it within the Recipe

UI of applying a recipe with inputs

Recipes vs. Modules

Venn diagram of recipes and modules

Using recipes

Add a recipe to your site

Recipes are stored in a new top-level /recipes folder, typically outside of the web root

composer require drupal/drupal_cms_page
Note: For existing sites, make sure you use latest composer/installers / update your composer.json config from the recommended template

						"installer-paths": {
								...
								"recipes/{$name}": ["type:drupal-recipe"],
						},
					

Apply a recipe using CLI

Drupal core (must run this inside web root)

php core/scripts/drupal recipe  ../recipes/drupal_cms_page

Drush 13 (works everywhere)

drush recipe ../recipes/drupal_cms_page

Using Project Browser

Experimental, Drupal Core Initative

Screenshot of project browser UI

Creating recipes

To distribute a recipe: composer.json


							{
									"name": "drupal/drupal_cms_page",
									"description": "Adds a content type for simple pages.",
									"type": "drupal-recipe",
									"license": ["GPL-2.0-or-later"],
									"require": {
											"drupal/core": ">=10.4",
											"drupal/drupal_cms_content_type_base": "~1.0.2"
									},
									"version": "1.0.2"
							}
						

recipe.yml (basics)


							# Human readable name & description
							name: Basic page
							description: Adds a content type for simple pages.
							# Group/Package of the recipe
							type: Drupal CMS
							# Apply other recipes first
							recipes:
								- drupal_cms_content_type_base
							# Install modules & themes
							install:
  							- menu_link_content
						

recipe.yml (strict)


							config:
							  # Fail if config exists and does not match (default)
								strict: true
							  # Do not fail if config exists and does not match
								strict: false
							  # Only fail on specific config entities
							  strict:
							    - field.storage.body
						

recipe.yml (config install)


							config:
							  # By default, only simple config of modules is installed
								import:
							    node:
							      - node.type.page
							    menu_link_content: *

						

recipe.yml (config actions)


							config:
							  actions:
									user.role.content_editor:
										grantPermissions:
											- 'create person content'
											- 'delete person revisions'
											- 'delete any person content'
											- 'edit any person content'
									workflows.workflow.basic_editorial:
										addNodeTypes: ['person']
									scheduler.settings:
										simpleConfigUpdate:
											hide_seconds: true
						

Recipe config

Just like modules, export and put the config in the config/ folder.


							core.entity_form_display.node.page.default.yml
							core.entity_view_display.node.page.card.yml
							core.entity_view_display.node.page.default.yml
							core.entity_view_display.node.page.teaser.yml
							field.field.node.page.field_content.yml
							field.field.node.page.layout_builder__layout.yml
							node.type.page.yml
							pathauto.pattern.page_content.yml
						

Reminder: Remove uuid and _core keys

Default content

Export with drush commands from the default_content project


							$ composer require drupal/default_content
							$ drush en default_content
							$ drush dcer --folder=../recipes/my_recipe/content node 4
							node/ce429bd2-2a91-48cf-9890-8b7bea56de52.yml
							media/9f589217-c3bd-41eb-a0b5-7e8b6cd0e80b.yml
							taxonomy_term/d48adeba-e388-4e82-820b-98bbd2f35a46.yml
							file/DrupalCon-Barcelona-2024 - credit-Bram-Driesen.jpg
							file/4bb02092-717b-44c8-9147-be3821c244c6.yml
						

Inputs

Example drupal_cms_google_analytics


							input:
								property_id:
									data_type: string
									prompt:
										method: ask
							    form:
							      '#type': textfield
							  actions:
									google_tag.container.drupal:
										set:
											property_name: tag_container_ids
											value:
												- ${property_id}
						

Create Config actions

ConfigAction Plugin or an attribute on a config entity method


							  #[ActionMethod(adminLabel: new TranslatableMarkup('Enable'))]
  							public function enable() { }
						

Recipe events

Allows to act when certain recipes are installed. Necessary when reacting to config entities or content.


						

Limitations, Caveats, Learnings

No installed state

Drupal doesn't know if you've already applied a recipe or not.

That also applies to recipes it depends on. Every time a recipe is applied, all recipes it depends on are applied again.

Strict mode

In strict module, a recipe can only be enabled if the configuration it provides does not exist yet or is an exact match.

Initially always strict, currently strict by default: drupal.org/i/3478699.

Reason: Config dependency conflicts, e.g. mismatching type between field and field storage

Strict challenges: Dependencies

  1. Apply Recipe A: News
  2. Add a field to News node type
  3. Apply Recipe B that depends on A
  4. Error

Strict challenges: Core changes

Open bug report against redirect module

  1. Your recipe/module provides a view
  2. Drupal core adds a new option for a field plugin, automatically sets default of that
  3. Installed config no longer matches recipe
  4. Reapply the recipe
  5. Error

Strict mode: Solution?

Short term: disable strict mode or only use for field storages

Long term: Better config validation, fields should ensure the storage has a matching type

Module configuration

  • Like config deployment: Modules installed only with simple config
  • Alternative to install profile config overrides
  • Problems
    • Recipes do not own module configuration
    • Different result if the module is already installed
    • Modules might rely on their config: default language, node_add_body()
    • More likely to break as modules extend their config

Module configuration: Solution?

Long term alternative: Modules should only ship required/minimal config, use optional/recipes for opinionated/example configuration. Maybe a new config/recommended?

No optional/conditional configuration

Modules have an optional config folder, that is only installed if all dependencies are met. Recipes don't.

Use case: Multilingual, ship content settings if site is multilingual

More detailed thoughts in Drupal CMS Multilingual issue: #3485172

Composer Dependencies

Removing a module that a recipe no longer depends on will remove that from existing sites that might have it installed.

Solution: Drupal will "unpack" recipe dependencies and add them directly to composer.json #3355485

Drupal CMS & recipes

A quick look under the hood

Drupal CMS is 99% recipes

An install profile to bootstrap the installation, then it uninstalls itself

You install Drupal CMS, but immediately after, it's just "Drupal"

The options you pick are all Recipes

Version 1.0: English only

No language selection in Installer

Multilingual track postponed, currently no track owner

Automated subtree split

  • Development happens in drupal.org/project/drupal_cms
  • Gitlab CI Job then subtree-splits into dedicated drupal.org project for each recipe
  • Similar to Symfony project
  • Results in installable recipes that are put in /recipes folder
  • Might be supported for all projects in the future?

Provides a custom olivero subtheme

  • Recipes can not provide CSS/Templates
  • Adds some styling for specific provided recipes
  • Will be replaced with a dedicated Drupal CMS theme later
  • Idea: Standalone Single Directory Components (SDC)?

Recipe project browser

Currently limited to local recipes and locked down to specific Drupal CMS recipes.

Allow downloading recipes: #3413567

Paragraphs & Recipes

Paragraphs Demo

Paragraphs comes with a demo module that creates some example multilingual paragraph types, content and search integration.

Sole purpose is throwaway demo sites.

Idea: Convert to multiple recipes that can be used as a starting point for real sites as well.

#3486479

Challenges: Limited UI/Exposure

A module can be installed through the UI with Drupal core

A recipe requires drush commands or project_browser (will be in core)

Currently not a good fit for the target audience

Challenges: A module is still required

  • CSS
  • Example conversion plugins
  • Event subscriber: Set created default content node as frontpage

Challenges: Distribution/Discoverability

Recipes within a module are currently not really discoverable

Adopt Drupal CMS Subtree Split?

Would also require the demo module to be split

Challenges: Granularity

A recipe per paragraph type?

A single recipe for all?

Something in between?

(My) Conclusion

  • Potential to ease onboarding for new users
  • Completely sidestepping the update problem is an interesting option
  • But: Dealing with existing sites remains the hard part for a distribution, see Drupal CMS 1.0.1
  • Benefits for us currently not clear enough to refactor our existing product

Questions