Skip to content

Recipe authoring

Compose your recipe out of existing recipes

A key part of building a recipe is to identify existing recipes to reuse in yours.

Site builders can apply multiple recipes on a given site. For example, they might first apply an event recipe, then an article recipe, then a SEO recipe. For this to work well, all these recipes have to share the same base configuration.

Say you're creating an event recipe that will provide an event content type and related configuration, including permissions related to the event content type. Those permissions need to be applied to one or more roles, but typically the event recipe shouldn't provide those roles itself. If it does, another recipe that needs the same roles will need to apply the event recipe--whether or not it's related to events.

Guidelines for interoperable recipes

  • Start with core recipes.
  • Prior to Drupal 10, short of patching, there was no way to build on the install profiles like standard that shipped with Drupal core. Even if you needed the exact same configuration as was in standard, you had to provide it yourself.
  • Now the opposite is true--pretty much every conceivable set of recipes should build on the recipes in core. Doing so is the best and easiest way to ensure your recipes integrate well with others.
  • Put reusable config in its own recipe.
  • As a rule, where a config entity is applicable to many different use cases, it should go in its own recipe. For example, a given recipe may call for both, neither, or one of the roles "administrator" and "content editor" as was done in core's standard recipes. Similarly, any field storage that's applicable to multiple content types goes in its own recipe.
  • Use selective enhancement for maximum flexibility.
  • For example, not all recipes that require an article content type will also require tags. So the tags vocabulary goes into its own recipe, an article recipe doesn't require tags, and there's a bridging recipe that selectively enhances article with tags.
  • Selective enhancement often means using config actions to integrate one recipe with another. For example, a bridging recipe that selectively enhances an article content type with tags would use config actions to add a tags field to the entity view and form displays provided by an article recipe.

The composer.json file

If it's going to be maintained in its own repository, a recipe should have a composer.json file. See the recipe documentation for a sample. The composer.json file is used to assemble the code for your recipe.

The logo.png file

For recipes to appear in Project Browser, they need to have a logo.png in their Drupal.org project repository.

  • The logo must be 512x512 square dimension in PNG format without animations.
  • Suggested file size should be 10k or less.
  • Use a lossy image tool (such as pngquant) to reduce file size while keeping the image quality at around 80%.
  • Place the logo in the root directory of the Git repository for your project, on the default branch. The logo file should be named logo.png. The logo will also be placed on Drupal.org project pages to the left of the project name. Logos are cached and may take up to an hour to show.

(Please note: you should not round the corners in the PNG itself, unless there is some compelling branding reason. We will round corners when displaying in the Project Browser.)

The recipe.yml file

The recipe.yml file is where your recipe is defined. It can do a number of things. Again, see the recipe documentation for a sample.

Basic information on a recipe

  • Name. A short name for the Drupal recipe.
  • Description. A longer description of the Drupal recipe.
  • Type. Groups related Drupal recipes together. See a list of recommended recipe types to use.

Example for a recipe for a tags taxonomy:

name: Tags
description: 'Provides "Tags" taxonomy vocabulary and related configuration. Use tags to group content on similar topics into categories.'
type: 'Taxonomy'

Apply other recipes

The recipes section of a recipe.yml file lists other recipes to apply before applying this one.

In some cases, this is all a recipe will do. For example, a standard_roles recipe might simply apply two other recipes, each providing a user role:

recipes:
  - administrator_role
  - content_editor_role

Install modules and/or themes

The install section of a recipe.yml file lists modules or themes to install:

install:
  - field
  - node
  - taxonomy

When the modules or themes are installed, any "simple configuration" they provide will be installed. The installation of the module here differs from installing a module in the UI or Drush as configuration entities are not imported by default. This is explained further in the next section.

Ask for input

Recipes can request input from the user, and use that input as replacement tokens in config actions.

An example of a recipe that needs input is core's feedback_contact_form recipe, which needs to set the recipient email address of the site-wide contact form. It defines a single input, called "recipient", like so:

input:
  recipient:
    # "primitive" data types like string, integer, float, boolean, email, etc.
    data_type: email
    description: 'The email address that should receive submissions from the feedback form.'
    constraints:
      NotBlank: []
    form:
      '#type': email
      '#title': 'Feedback form email address'
    prompt:
      # ask, askHidden, choice, and confirm.
      method: ask
      arguments:
        question: 'What email address should receive website feedback?'
    default:
      source: config
      config: ['system.site', 'mail']

Then you can use the ${recipient} replacement token in config actions.

config:
  actions:
    contact.form.feedback:
      setRecipients:
        - ${recipient}
    system.site:
      simpleConfigUpdate:
        slogan: 'This is the home of ${recipient}'

As of Drupal 11.1.2 (not in Drupal 10), you can use the input tokens to dynamically target config entities, with certain limitations.

config:
  actions:
    node.type.${content_type}:
      setDescription: 'Changing the description of a user-chosen node type!'

You can only use the tokens in the identifying parts of the config entity ID, for example, a node type's actual machine name, node.type.${content_type}.

Input tokens can also only be used on configuration entity names. The can't be used in simple config names.

config:
  actions:
    # Tokens can't be used to select the entity type.
    ${module_name}.type.foo:
      doSomething: here
    # Tokens can't be used on simple configs.
    system.${config_name}:
      simpleConfigUpdate:
        someKey: some value

Provide configuration

In your recipe, you can provide configuration in a few different ways.

Config Folder

You can provide configuration directly in the recipe. This config doesn't need to be listed in the recipe.yml file. Just put it in a config directory and it will be installed when your recipe is applied.

createIfNotExists Config Action

Second, you can create configuration entities if they do not exist using the createIfNotExists config action.

config:
  action:
    user.role.clammer:
      # If this role already exists, then this action has no effect.
      # If it doesn't exist, we'll create it with the following values.
      createIfNotExists:
        id: clammer
        label: Clammer
        weight: 4
        is_admin: false

Config Import

You can install additional configuration entities from modules you install, beyond the simple configuration that's always installed.

Please read the Configuration API documentation for a great explanation of Simple Configuration vs. Configuration Entities

To ensure most choices in a recipe are intentional by default, your recipe must explicitly say what it wants to do with the config of each module it is installing, even optional configuration. You can do three things:

  • Import all the module's config, even the module's optional configuration.
  • Import none of the config.
  • Import some of the config by listing it explicitly.

For a given module or theme, you have two options. First, you can use a * wildcard to install all configuration the module provides, just as would happen when the extension is installed "normally" (not via a recipe).

In most cases, this is the recommended approach. Doing so ensures the module or theme behaves as designed. Here's what that looks like in a recipe:

config:
  import:
    field: '*'
    node: '*'
    taxonomy: '*'

If you do not want to import any of the config, you can leave the module out of the config:import section, or include it with NULL like this:

config:
  import:
    views: NULL

For more advanced and specialized uses, there's the option of bypassing the usual config installation. Because it's easy to break things this way, it should be used only when there's a compelling use case to install an extension without all of its config.

Where there is a use case for cherry-picking specific config items, it's done by listing the specific items you want to install.

For example, this recipe snippet would install only the large image style as provided by the core image module:

config:
  import:
    image:
      - image.style.large

It's also worth pointing out that any config actions run after any module config is imported, so config actions are able to modify any existing config, including any that was imported by the recipe.

Comparing configuration using strict

The recipe system compares configuration from your recipe and the modules you bring in against your site's active configuration. By default it requires that they be identical in every detail, or it throws an error.

  # The default.
  strict: true

You can set this to false to have the recipe runner skip the config import if it finds anything different.

  strict: false

A recipe can choose to opt only certain config into the strict checking. If you specify individual config files to be treated strictly, then all others will be treated lenient.

  strict:
    - field.storage.field_oysters
    - field.storage.field_quahogs

Provide content

Recipes can provide default content. Any kind of content entity is supported, and recipes can include translated content too!

The content should be included in a content directory, next to recipe.yml. That directory should contain the content exported as a set of YAML files. These files can have any name, and be at any level of nesting, but the best practice is to segment them by their entity type. So, for example:

my_recipe/
  recipe.yml
  content/
    node/
      node-1.yml
      node-2.yml
    menu_link_content/
      link-1.yml
      link-4.yml
    taxonomy_term/
      term-3.yml
      term-39.yml

The content files must be created using the 2.x version of the contributed Default Content module. Drupal core does NOT currently have a way to export content.

If you have a site you want to export content from to be included in a recipe, you should export it piecemeal, using Drush. You'll need to know the ID of the content you want to export, as well as its entity type ID. So, to export node 39 into a recipe, along with all of its dependencies (for example, the user who created it, and any taxonomy terms or files that it references):

drush dcer node 39 --folder=/path/to/your/recipe/content

If you want more granular control, you can export a content entity without any of its dependencies. For example:

drush dce node 39 > /path/to/your/recipe/content/node-39.yml

When the recipe is applied, all of the content included with it will be imported. Note that any imported content might NOT have the same ID that it had when you exported it. So if you exported it as node 39, it might be imported as node 42. (It will vary depending on what content the destination site already has in it, if any.)

See core/tests/fixtures/recipes/only_content for an example of a recipe that ships default content.

Converting install profiles to recipes

A common task as Drupal distributions migrate to Drupal 10 will be to convert an existing install profile into a set of recipes. The same is being done in Drupal core with the standard, minimal, demo_umami, and other core install profiles. Here are some tips and notes.

Convert config overrides to config actions

In Drupal 8 and 9, an install profile can re-provide configuration provided by an extension, effectively overriding the extension-provided version.

In recipes, rather than re-providing the full config item, selective changes are made through config actions. So when converting an install profile to a set of recipes, any config overrides in the install profile need to be converted into config actions.

Suggested steps:

  1. Build the install profile's code base. This step may involve running composer. If you're working on converting an install profile in Drupal core, you only need the Drupal core code base.
  2. Identify config entities provided by the install profile that override ones provided originally by a non-test extension.
  3. If you're not sure whether a given config entity provided by the install profile is an override, one way to find out is to search for the config entity's .yml file in the code base. If there's more than one result, look for one that's in an extension (excluding test modules).
  4. For example, the demo_umami install profile includes the configuration file filter.format.basic_html.yml.
    • Use your favourite IDE to search the code base for files with that name. To instead search using the find command from the root of a Drupal install: $ find . -type f -name filter.format.basic_html.yml
    • If the file you're converting is in an install profile that's part of Drupal core, alternately you could search for the file using the Drupal API site. Either way you should find two results, one of them in the core filter module. In this case, then, the answer is yes, the install profile is overriding this config entity.
  5. In your install or repository, diff the extension-provided version with that in the install profile. Sample diff command from the root of a git clone of Drupal core repository: $ diff -u core/profiles/standard/config/install/filter.format.basic_html.yml core/profiles/demo_umami/config/install/filter.format.basic_html.yml
  6. Looking at the diff, determine what's changed. For each change, determine whether there is a config action either implemented or proposed. For now, the best places to look are:
  7. The "Available config actions" section of the config actions documentation.
  8. Child issues of Determine which core config entity methods should be config actions If you find one, use it. For details on how to write your config action, see the config actions documentation. Keep in mind that if the action you need is proposed but not yet implemented you may need to wait or help in the issue queue to get it in. The good news is that in most cases implementing a new config action is relatively straightforward--it involves adding a single line that marks an existing method as an action. If you don't find a relevant config action, continue with the steps below.
  9. Identify the config entity type that corresponds to the config item.
  10. If you're unsure, you can find it using the name of the configuration file. For background, see the notes on config_prefix in the configuration API documentation for module developers For filter.format.basic_html.yml, the second element of the config file name is format so we're looking for a config entity with the config_prefix value of "format". Use your favourite IDE to search for the string config_prefix = "format" in a .php file. Or using grep from the root of your Drupal install or repository: $ grep -inr --include \*.php 'config_prefix = "format"' . Either way should get you to the filter_format config entity type and its FilterFormat class.
  11. Look for a relevant public method on the config entity class.
  12. Continuing with the example of filter.format.basic_html.yml, one change is that two new filters have been added, filter_autop and media_embed. Looking through the public methods of FilterFormat, the relevant method here is FilterFormat::setFilterConfig().
  13. You don't need to worry about any differences you see in config dependencies. These will be taken care of automatically when the config item is saved and dependencies are recalculated.
  14. For each method you found you need to use and for which there isn't a config action already implemented or proposed, open a new child issue of Determine which core config entity methods should be config actions. To get an idea of what goes into the issue, see one of the child issues that have already been opened.