{"@attributes":{"version":"2.0"},"channel":{"title":"Drupal CMS","description":"Stay informed about developments in Drupal CMS.","link":"https:\/\/www.drupal.org\/project\/drupal-cms","item":[{"title":"#3499319: Reponsive images should use sizes attribute for container-width sensitive image loading","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3499319.md","guid":"019ea6af-201a-7673-acc8-e5d605006e5c","pubDate":"Mon, 08 Jun 2026 06:31:45 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal unknown (0)<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3499319\">#3499319<\/a> \u00b7 2 contributors \u00b7 6 comments<\/li>\n<li><strong>Followers:<\/strong> 5<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>Drupal CMS responsive image styles now use the HTML <code>sizes<\/code> attribute approach instead of mapping a single image style per breakpoint. All aspect-ratio-specific styles (e.g. <code>16_9_medium<\/code>, <code>3_2_large<\/code>, <code>4_3_wide<\/code>) emit a <code>srcset<\/code> of multiple focal-point-cropped WebP candidates and a <code>sizes: 100vw<\/code> hint, letting the browser pick the right resolution automatically. The responsive image styles are also decoupled from the Olivero theme: they now depend on custom breakpoints (<code>custom.sm<\/code> through <code>custom.xl<\/code>) defined and managed through an ECA model, so Olivero no longer needs to be installed just for image configuration to work.<\/p>\n<h2>Impact<\/h2>\n<p>No upgrade or configuration changes required.<\/p>\n<h2>Technical details<\/h2>\n<p>All <code>responsive_image.styles.*<\/code> configs shipped by <code>drupal_cms_media<\/code> and <code>drupal_cms_starter<\/code> use <code>image_mapping_type: sizes<\/code> under the <code>custom.sm<\/code> breakpoint with <code>sizes: 100vw<\/code>. Each style lists between one and five focal-point-cropped WebP <code>sizes_image_styles<\/code> at different pixel widths; the browser resolves which file to request based on its own viewport arithmetic. Hero styles (<code>hero_large<\/code>, <code>hero_wide<\/code>) continue to use <code>image_mapping_type: image_style<\/code> because they have a fixed layout that spans known breakpoints rather than a fluid container.<\/p>\n<p>The breakpoint group was previously <code>olivero<\/code>, requiring Olivero to be installed alongside the image recipe. MR623 (tracking issue <a href=\"https:\/\/www.drupal.org\/node\/3543740\">#3543740<\/a>) introduced an ECA model (<code>eca.eca.define_breakpoints<\/code>) that fires on <code>eca_render:breakpoints_alter<\/code> and injects four <code>custom.*<\/code> breakpoints at runtime via <code>eca_render_alter_breakpoint<\/code> actions. All responsive image styles were updated to reference <code>breakpoint_id: custom.sm<\/code> and <code>breakpoint_group: custom<\/code>, with an enforced config dependency on the ECA model so that deleting the model cascades to delete the styles. The recipe's install list replaced <code>olivero<\/code> with <code>bpmn_io<\/code>, <code>eca_base<\/code>, <code>eca_render<\/code>, <code>eca_ui<\/code>, and <code>modeler_api<\/code>.<\/p>\n<p>A related simplification (part of the same effort) removed the &quot;large&quot; view mode options (<code>16_9_large<\/code>, <code>1_1_square_large<\/code>, etc.) from the CKEditor allowed embedded media display modes, since a narrower content column means &quot;large&quot; in-body images are no longer meaningfully distinct from &quot;medium.&quot;<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: January 13, 2025<\/li>\n<\/ul>\n<h3>Collaboration<\/h3>\n<p>tonypaulbarker commented that the editor's explicit size choice when embedding an image (small vs. medium in a narrow column) cannot be replaced by the <code>sizes<\/code> attribute alone, because responsive images decide based on viewport\/container math rather than editorial intent. This shaped the outcome: the <code>sizes<\/code> approach was adopted for resolution selection within each named style, but the distinct small\/medium\/large\/wide view modes were kept so editors retain control over the display size they intend.<\/p>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3591375: Automatically copy font and color CSS files to Drupal root if a site template sets up a Mercury-based theme by default","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3591375.md","guid":"019e9255-6552-7006-b520-cfaef90ee748","category":"Module maintainers","pubDate":"Tue, 02 Jun 2026 21:28:43 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3591375\">#3591375<\/a> \u00b7 1 contributor \u00b7 2 comments<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>When a site template sets a Mercury-based theme as the site default, Drupal CMS Helper now automatically copies that theme's non-minified CSS files (typically <code>theme.css<\/code> and <code>fonts.css<\/code>) from the theme directory into the Drupal web root. Because Canvas does not yet offer a UI for overriding fonts and colors, Mercury-based themes expose these as plain CSS files that developers can edit. Placing copies in the web root gives developers an obvious, version-control-friendly location to make those customizations without touching the theme package itself. The copy operation uses <code>FileExists::Error<\/code>, so it will not overwrite files a developer has already customized. If the web root is not writable, or the files already exist, the copy attempt is silently skipped.<\/p>\n<h2>Impact<\/h2>\n<p>Affects module maintainers.<\/p>\n<ul>\n<li><strong>New API:<\/strong> New static <code>RecipeSubscriber::isMercuryBased(Extension $theme): bool<\/code> method for checking if a theme is Mercury or a Mercury sub-theme.<\/li>\n<\/ul>\n<h2>Technical details<\/h2>\n<p>The logic is added to <code>RecipeSubscriber::onApply()<\/code> in <code>drupal_cms_helper<\/code>. After handling the existing front-page alias and user-cache concerns, it now also runs when <code>$event-&gt;recipe-&gt;type === 'Site'<\/code>. It reads the current default theme from <code>system.theme<\/code> config, calls the new static <code>RecipeSubscriber::isMercuryBased(Extension $theme)<\/code> helper (which returns <code>true<\/code> when the theme machine name is <code>mercury<\/code> or when its <code>generator<\/code> info key starts with <code>mercury:<\/code>), and only proceeds if the web root (<code>$appRoot<\/code>) is writable. It then calls <code>LibraryDiscoveryInterface::getLibraryByName()<\/code> to retrieve the theme's <code>global<\/code> library, filters its <code>css<\/code> entries to those where <code>minified<\/code> is empty\/false, and copies each file to <code>$appRoot . DIRECTORY_SEPARATOR . basename($origin)<\/code> via <code>FileSystemInterface::copy()<\/code> with <code>FileExists::Error<\/code>. A caught <code>FileException<\/code> is silently swallowed so the recipe apply does not fail on a pre-existing file. Four new constructor arguments are injected via autowiring: <code>ThemeExtensionList<\/code>, <code>FileSystemInterface<\/code>, <code>LibraryDiscoveryInterface<\/code>, and the <code>app.root<\/code> parameter. A new kernel test class <code>RecipeSubscriberTest<\/code> covers both the Mercury-theme path (expects <code>fonts.css<\/code> and <code>theme.css<\/code> to be present and byte-for-byte identical to the theme source) and the non-Mercury path (expects no CSS files copied).<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: May 27, 2026<\/li>\n<\/ul>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3580694: The project template should always place config outside the web root by default","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3580694.md","guid":"019d857f-33e4-728b-bbba-950cfdc8e447","category":"Site owners","pubDate":"Tue, 24 Mar 2026 21:33:54 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Changes:<\/strong> 4 files, +21\/\u22125 \u00b7 txt +10 \u00b7 php +8\/\u22125 \u00b7 json +3<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/1accadea08e646807555a3b72a5441695ffbb1fe\">1accade<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3580694\">#3580694<\/a> \u00b7 1 contributor \u00b7 6 comments<\/li>\n<li><strong>Followers:<\/strong> 3<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>The Drupal CMS project template now places the configuration sync directory outside the web root by default. New sites created from the template will store exported configuration in a <code>config\/sync<\/code> directory at the project root, preventing direct web access to configuration files. This follows Drupal security best practices. Existing sites are not affected.<\/p>\n<h2>Impact<\/h2>\n<p>Affects site owners.<\/p>\n<ul>\n<li><strong>Config change:<\/strong> New projects place config sync directory at <code>..\/config\/sync<\/code> (outside web root) by default<\/li>\n<\/ul>\n<h2>Technical details<\/h2>\n<p>The project template already uses a relocated web root at <code>web\/<\/code>. This change adds a <code>config\/sync<\/code> directory as a sibling to the web root, keeping configuration files outside the publicly accessible directory tree.<\/p>\n<p>The implementation uses the drupal-scaffold Composer plugin's file-mapping feature to append settings to <code>default.settings.php<\/code>. A new <code>assets\/append-default.settings.php.txt<\/code> file contains PHP code that sets <code>$settings['config_sync_directory']<\/code> to <code>realpath('..\/config\/sync')<\/code> if that directory exists. The template includes an empty <code>config\/sync<\/code> directory with a <code>.gitkeep<\/code> file.<\/p>\n<p>The test suite in <code>SiteTemplateInstallTest.php<\/code> was updated to mirror the new <code>assets\/<\/code> directory along with <code>composer.json<\/code>. The test switched from using <code>copy()<\/code> to <code>Filesystem::mirror()<\/code> with a Finder to include both the assets directory and composer.json file.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: March 21, 2026<\/li>\n<li>Committed: March 24, 2026 (2 days and 2 commits later)<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>phenaproxima (Acquia)<\/li>\n<li>brianperry<\/li>\n<li>dorficus (HeroDevs)<\/li>\n<\/ul>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3579163: Add support for listing paid site templates in the installer","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3579163.md","guid":"019e1e2d-9f04-70b3-be57-3b6ac5044bdb","category":"Module maintainers","pubDate":"Tue, 17 Mar 2026 22:08:57 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Closed (fixed)<\/li>\n<li><strong>Changes:<\/strong> 6 files, +252\/\u221235 \u00b7 php +236\/\u221235 \u00b7 yml +16<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/689f58926839a8c55a71b617679c1df6dde3c90d\">689f589<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3579163\">#3579163<\/a> \u00b7 3 contributors \u00b7 44 comments<\/li>\n<li><strong>Followers:<\/strong> 4<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>The Drupal CMS installer now supports paid (premium) site templates alongside free ones. Premium templates display a price and a &quot;Buy for $X&quot; link that opens the vendor's purchase page in a new tab. After purchasing, the user enters a license key through a modal dialog; the installer validates the key against the vendor's Composer repository (and optionally a lightweight server-side validation endpoint), saves it as a bearer token in <code>auth.json<\/code>, and then proceeds with installation as normal. Free templates also receive a &quot;Free&quot; badge for visual parity. The blank starter template is always listed first and pre-selected. Site hosts and testing environments can place a <code>site-templates.php<\/code> file in the site directory to override the remotely curated template list entirely; this is documented as an official extension point.<\/p>\n<h2>Impact<\/h2>\n<p>Affects module maintainers.<\/p>\n<ul>\n<li><strong>New API:<\/strong> New <code>site-templates.php<\/code> extension point in the site directory: returning an iterable of template arrays overrides the remote <code>site-templates.yml<\/code> list in the Drupal CMS installer.<\/li>\n<\/ul>\n<h2>Technical details<\/h2>\n<p>The central addition is the <code>SiteTemplate<\/code> final readonly value object (<code>Drupal\\drupal_cms_installer\\SiteTemplate<\/code>, marked <code>@internal<\/code>). It normalizes both locally present recipes and remotely hosted Composer packages into a single shape, carrying: <code>name<\/code>, <code>description<\/code>, <code>locator<\/code> (file path or Composer package name), <code>repository<\/code>, <code>authorization<\/code>, <code>links<\/code> (as <code>Link<\/code> objects), <code>price<\/code>, <code>purchaseUrl<\/code>, <code>keyValidationUrl<\/code>, and a <code>getScreenshot()<\/code> method that base64-encodes local <code>.webp<\/code> files so they can be served from outside the web root. The static factory <code>SiteTemplate::createFromRecipe()<\/code> wraps an existing <code>Recipe<\/code> object.<\/p>\n<p><code>SiteTemplateForm<\/code> was refactored to work with <code>SiteTemplate<\/code> instances throughout. The <code>access_key<\/code> field is now a <code>#type =&gt; container<\/code> holding one textfield per premium template, each shown conditionally via <code>#states<\/code> when its associated radio is selected. Validation logic moved from <code>submitForm()<\/code> into <code>validateForm()<\/code>: alternate Composer repositories are registered via <code>ComposerExecutor::execute('repository', 'add', ...)<\/code> using the xxh3 hash of the host+port as the repository name, the bearer token is written via <code>composer config bearer.&lt;host&gt; &lt;key&gt;<\/code>, then <code>composer show --all &lt;package&gt;<\/code> confirms the package is visible; on failure the token is unset and a form error is set. The site directory may also contain a <code>site-templates.php<\/code> returning an iterable of template arrays; when present it overrides the remote <code>site-templates.yml<\/code> list entirely and is treated as <code>@api<\/code>.<\/p>\n<p>The <code>form_element__site_template<\/code> Twig template was substantially expanded to render links, price, purchase button, and a &quot;Already purchased? Enter license key&quot; button that opens a <code>&lt;dialog&gt;<\/code> element. The <code>premium-helper.js<\/code> behavior manages the dialog lifecycle, applies a UUID input mask, performs debounced server-side validation (via <code>Drupal.debounce<\/code> and a <code>fetch()<\/code> against <code>keyValidationUrl<\/code>), and updates status indicators (loading spinner, valid checkmark, invalid X). The <code>site-templates.yml<\/code> format was extended with <code>purchase.price<\/code>, <code>purchase.url<\/code>, <code>purchase.validation_url<\/code>, <code>links<\/code>, and <code>package.authorization<\/code> keys. A new <code>SiteTemplateInstallTest<\/code> build test provides end-to-end coverage of the bearer-token flow using an embedded PHP server that simulates a protected Composer repository.<\/p>\n<h2>Upgrade<\/h2>\n<p>Site hosts who want to override the installer's template list can create a file at <code>sites\/default\/site-templates.php<\/code> (or whichever site directory is in use) that returns an iterable of template arrays:<\/p>\n<pre><code class=\"language-php\">&lt;?php\n\/\/ sites\/default\/site-templates.php\nreturn [\n  'my_template' =&gt; [\n    'name' =&gt; 'My Template',\n    'description' =&gt; 'A custom site template.',\n    'screenshot' =&gt; 'https:\/\/example.com\/screenshot.webp',\n    'package' =&gt; [\n      'name' =&gt; 'vendor\/my-template',\n      'repository' =&gt; 'https:\/\/packages.example.com',\n      'authorization' =&gt; 'bearer',\n    ],\n    'purchase' =&gt; [\n      'price' =&gt; 99,\n      'url' =&gt; 'https:\/\/example.com\/buy',\n      'validation_url' =&gt; 'https:\/\/example.com\/validate', \/\/ optional\n    ],\n    'links' =&gt; [\n      ['text' =&gt; 'Demo', 'url' =&gt; 'https:\/\/example.com\/demo'],\n    ],\n  ],\n];\n<\/code><\/pre>\n<p>When this file exists and returns an iterable, it replaces the remote <code>site-templates.yml<\/code> list entirely. This is an official extension point (<code>@api<\/code>).<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: March 13, 2026<\/li>\n<li>First commit: March 14, 2026 (1 day later)<\/li>\n<li>Last commit: March 17, 2026 (3 days and 14 commits later)<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>phenaproxima (Acquia)<\/li>\n<li>mherchel (Dripyard)<\/li>\n<li>andyg5000 (Dripyard)<\/li>\n<li>pameeela (Technocrat)<\/li>\n<\/ul>\n<h3>Collaboration<\/h3>\n<p>phenaproxima led the backend architecture across multiple merge requests, writing the <code>SiteTemplate<\/code> value object, the Composer integration, and the end-to-end build tests. mherchel drove the frontend: the modal dialog UX, the UUID input masking, and the JavaScript state machine for license key validation. pameeela reviewed the design in a Zoom call early on and contributed the final round of CSS styling tweaks (commit <code>ab79bb57<\/code>). The thread also notes that andyg5000 (Dripyard) was consulted during the design phase and provided the initial use-case requirements for paid templates.<\/p>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3579729: Add an `overwrite` option to `drush site:export`","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3579729.md","guid":"019d857f-33e4-728b-bbba-94efa4f6944c","pubDate":"Tue, 17 Mar 2026 16:37:23 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Changes:<\/strong> 3 files, +45\/\u221210 \u00b7 php +45\/\u221210<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/73ee652134af85e6021a435573ec8db1c62b5c97\">73ee652<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3579729\">#3579729<\/a> \u00b7 1 contributor \u00b7 6 comments<\/li>\n<li><strong>Followers:<\/strong> 1<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>The <code>drush site:export<\/code> command now supports an <code>--overwrite<\/code> option that allows you to re-export a site to an existing directory. Previously, the command would fail if the destination directory already existed, requiring manual deletion before exporting again. This makes it easier to iterate on site exports during development without manually cleaning up directories between runs.<\/p>\n<h2>Impact<\/h2>\n<p>No upgrade or configuration changes required.<\/p>\n<h2>Technical details<\/h2>\n<p>The <code>SiteExportCommand<\/code> class in the <code>drupal_cms_helper<\/code> module now accepts an <code>--overwrite<\/code> option. When this option is provided, the command skips the check that would normally fail if the destination directory exists.<\/p>\n<p>The underlying <code>SiteExporter::copyBaseRecipe()<\/code> method was also updated to exclude <code>config<\/code> and <code>recipe.yml<\/code> from the base recipe when mirroring files, in addition to the existing <code>content<\/code> exclusion. This ensures these key files are always regenerated during export. The <code>delete<\/code> option was removed from the <code>Filesystem::mirror()<\/code> call, which means overwriting will only update files that are part of the export, not delete everything in the destination first. This preserves any additional files in the destination directory (like <code>.git<\/code> directories from version control).<\/p>\n<p>The test coverage confirms that overwriting replaces exported files like <code>recipe.yml<\/code> while leaving untouched files like <code>.git<\/code> directories intact.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: March 17, 2026<\/li>\n<li>Committed: March 17, 2026<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>phenaproxima (Acquia)<\/li>\n<\/ul>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3579616: The site:export command should always use drupal_cms_site_template_base as a base, if available","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3579616.md","guid":"019d857f-33e4-728b-bbba-94ea95b66010","pubDate":"Tue, 17 Mar 2026 02:17:21 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Changes:<\/strong> 9 files, +161\/\u2212203 \u00b7 php +161\/\u2212203<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/c72081fdf12a56ca0555404b5c928d61c2f03844\">c72081f<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3579616\">#3579616<\/a> \u00b7 1 contributor \u00b7 7 comments<\/li>\n<li><strong>Followers:<\/strong> 2<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>The site export command now defaults to using the Drupal CMS site template starter kit as a base, making it easier to create new site templates. Running <code>drush site:export<\/code> with no arguments produces a complete, working recipe with documentation, tests, and a screenshot. The exported recipe is self-contained and does not depend on other recipes, simplifying the site template creation workflow for DrupalCon Chicago and the Drupal CMS marketplace.<\/p>\n<h2>Impact<\/h2>\n<p>No upgrade or configuration changes required.<\/p>\n<h2>Technical details<\/h2>\n<p>The <code>site:export<\/code> command previously allowed exporting to any base recipe and attempted to merge changes into existing recipe files. This approach was overcomplicated and error-prone for the primary use case: creating new site templates for the Drupal CMS marketplace.<\/p>\n<p>The command now automatically uses <code>drupal_cms_site_template_base<\/code> (the site template starter kit) as the default base if available. Both the Drush command and the UI export controller check for the starter kit's existence using a new <code>SiteExporter::getRecipePath()<\/code> method, which determines where Composer installs recipes by parsing the project's <code>composer.json<\/code>.<\/p>\n<p>The export process now completely overwrites <code>recipe.yml<\/code> and <code>composer.json<\/code> rather than merging changes. The generated <code>recipe.yml<\/code> contains only the site name, type, installed extensions, and config actions\u2014no references to other recipes. The <code>composer.json<\/code> file is rebuilt from scratch with requirements for all installed extensions, removing any starter kit development dependencies.<\/p>\n<p>This produces monolithic site templates that are self-contained and ready to publish. The export includes all content, configuration, documentation, tests, and supporting files from the starter kit (README, CI configuration, screenshots), but excludes the starter kit's sample content.<\/p>\n<p>The installer form no longer stores the selected base template in state, since the export functionality now handles this automatically.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: March 16, 2026<\/li>\n<li>Committed: March 17, 2026<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>phenaproxima (Acquia)<\/li>\n<\/ul>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3575283: Add the ability to export a site template as a ZIP file","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3575283.md","guid":"019d857f-33e4-728b-bbba-949a7bcf6bf5","category":"Module maintainers","pubDate":"Mon, 09 Mar 2026 00:30:57 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Closed (fixed)<\/li>\n<li><strong>Changes:<\/strong> 3 files, +75\/\u22122 \u00b7 php +75\/\u22122<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/dfd34d6c83764de4f9f968629d3781b45a40137f\">dfd34d6<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3575283\">#3575283<\/a> \u00b7 4 contributors \u00b7 25 comments<\/li>\n<li><strong>Followers:<\/strong> 6<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>Drupal CMS now ships a dedicated route at <code>\/admin\/config\/development\/site-export<\/code> (inside the <code>drupal_cms_helper<\/code> module) that exports the current site as a downloadable ZIP file containing a Drupal recipe. The route requires the <code>administer site configuration<\/code> permission and is intentionally not linked anywhere in the UI. Individual site templates, such as the &quot;Blank&quot; starter kit, can expose it by adding their own menu links. Sites installed from the blank starter kit (<code>drupal_cms_site_template_base<\/code>) will have their export automatically seeded from that base template, including scaffold files like <code>README.md<\/code> and CI configuration.<\/p>\n<h2>Impact<\/h2>\n<p>Affects module maintainers.<\/p>\n<ul>\n<li><strong>New API:<\/strong> New route <code>drupal_cms_helper.site_export<\/code> at <code>\/admin\/config\/development\/site-export<\/code> (marked <code>@api<\/code>); <code>SiteExporter::export()<\/code> gains an optional <code>?string $base<\/code> parameter for specifying a base recipe to seed the export.<\/li>\n<\/ul>\n<h2>Technical details<\/h2>\n<p>The new route <code>drupal_cms_helper.site_export<\/code> (marked <code># @api<\/code> in routing) maps to <code>ExportController::exportArchive()<\/code> (marked <code>@internal<\/code>). The controller reads the <code>site_template_base<\/code> state key, which the installer's <code>SiteTemplateForm<\/code> stores when a user chooses the <code>drupal_cms_site_template_base<\/code> recipe during setup. That path is forwarded to <code>SiteExporter::export()<\/code> as the new optional <code>?string $base<\/code> parameter.<\/p>\n<p><code>SiteExporter::export()<\/code> was extended with a <code>$base<\/code> argument (defaulting to <code>NULL<\/code>). When a base path is supplied and is a valid directory, the new private <code>copyBaseRecipe()<\/code> method mirrors the base recipe into the destination (excluding its <code>content\/<\/code> directory) and renames <code>*.example<\/code> files by stripping the <code>.example<\/code> suffix, so scaffold templates become active. The logic that previously lived inline in <code>SiteExportCommand<\/code> was consolidated into <code>copyBaseRecipe()<\/code>. The <code>.htaccess<\/code> deletion from the exported <code>config\/<\/code> directory was also moved into <code>SiteExporter::export()<\/code> itself, making all callers benefit from it automatically.<\/p>\n<p>An initial implementation added the export as a button on the core config export form via <code>hook_form_config_export_form_alter<\/code>, but was quickly reverted after UX review. The final design uses an invisible standalone route that site templates can surface where appropriate, and drops the earlier custom <code>PharData<\/code> class-exists access check in favor of the standard permission requirement alone.<\/p>\n<h2>Upgrade<\/h2>\n<p>Site template authors who call <code>SiteExporter::export()<\/code> directly can now pass a base recipe path as the second argument:<\/p>\n<pre><code class=\"language-php\">\/\/ Before (still works, $base defaults to NULL)\n$siteExporter-&gt;export($destination);\n\n\/\/ After: optionally seed the export from a base recipe\n$siteExporter-&gt;export($destination, '\/path\/to\/base-recipe');\n<\/code><\/pre>\n<p>To expose the export route in a custom site template, add a menu link entity pointing to <code>drupal_cms_helper.site_export<\/code>. No code changes are required to use the route; a user with <code>administer site configuration<\/code> can already access <code>\/admin\/config\/development\/site-export<\/code> directly.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: February 23, 2026<\/li>\n<li>First commit: February 23, 2026<\/li>\n<li>Last commit: March 9, 2026 (13 days and 8 commits later)<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>phenaproxima (Acquia)<\/li>\n<li>thejimbirch (Kanopi Studios)<\/li>\n<li>penyaskito (Acquia)<\/li>\n<li>pameeela (Technocrat)<\/li>\n<\/ul>\n<h3>Collaboration<\/h3>\n<p>The change took two distinct rounds to land. The first round merged quickly with a single approving review from thejimbirch, but phenaproxima reverted it hours later after pameeela flagged that the config management UI was a confusing home for the export feature. In the comment announcing the revert, phenaproxima immediately proposed the alternative: an invisible route that individual site templates could choose to surface with their own menu links. penyaskito suggested splitting the functionality into a separate <code>site_template_builder<\/code> module, but phenaproxima preferred keeping it inside <code>drupal_cms_helper<\/code> alongside existing dev tooling. pameeela approved the revised route-based approach, and the implementation was re-landed in that form.<\/p>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3574664: The `site:export` command should be able to export on top of another recipe","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3574664.md","guid":"019d857f-33e4-728b-bbba-948ce321ec76","pubDate":"Fri, 27 Feb 2026 19:33:22 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Closed (fixed)<\/li>\n<li><strong>Changes:<\/strong> 5 files, +127\/\u221212 \u00b7 php +127\/\u221212<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/867047915339821e056e5c2c47f5f96062c44a36\">8670479<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3574664\">#3574664<\/a> \u00b7 1 contributor \u00b7 13 comments<\/li>\n<li><strong>Followers:<\/strong> 1<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>The <code>drush site:export<\/code> command in Drupal CMS gains a <code>--base<\/code> option that lets site template authors seed an export from an existing recipe directory (such as <code>drupal_cms_site_template_base<\/code>). The base recipe's files are copied to the export destination first, preserving scaffolding like CI configuration, license files, and README documents, then the site's live configuration is exported on top. Three export bugs are also fixed: the command now refuses to overwrite an existing destination directory, the <code>system.site<\/code> front page path is converted from its internal system path to its URL alias in the exported config, and child menu link content entities now correctly record their parent menu link as a dependency in the exported content.<\/p>\n<h2>Impact<\/h2>\n<p>No upgrade or configuration changes required.<\/p>\n<h2>Technical details<\/h2>\n<p>In <code>SiteExportCommand<\/code>, the new <code>--base<\/code> option triggers a two-step copy before the export. First, <code>Symfony\\Component\\Finder\\Finder<\/code> selects all files from the base recipe path with <code>ignoreVCS(TRUE)<\/code>, <code>ignoreDotFiles(FALSE)<\/code> (so hidden files like <code>.gitlab-ci.yml<\/code> and <code>.tugboat<\/code> are included), and <code>notPath('content')<\/code> (so base recipe content is excluded). <code>Symfony\\Component\\Filesystem\\Filesystem::mirror()<\/code> writes those files to the destination. Second, a follow-up <code>Finder<\/code> pass locates any file whose name matches <code>*.example<\/code> or <code>.*.example<\/code> and renames each by stripping the eight-character <code>.example<\/code> suffix, activating dormant placeholder files such as <code>.gitattributes.example<\/code>. The command also checks for an existing destination directory upfront and returns <code>Command::FAILURE<\/code> immediately if it exists.<\/p>\n<p><code>GenericConfigurationListener<\/code> is changed from <code>final readonly<\/code> to <code>final<\/code> (mutable) so that <code>SiteExporter<\/code> can toggle a public <code>$convertFrontPagePathToAlias<\/code> flag at runtime. When the flag is <code>TRUE<\/code>, the listener's <code>__invoke()<\/code> method calls <code>AliasManagerInterface::getAliasByPath()<\/code> on the <code>system.site<\/code> <code>page.front<\/code> value before writing it to config storage, replacing the internal node path with its URL alias. <code>AliasManagerInterface<\/code> is injected via the updated constructor. This shim is marked for removal when core issue <a href=\"https:\/\/www.drupal.org\/node\/1503146\">#1503146<\/a> is released.<\/p>\n<p><code>DefaultContentSubscriber<\/code> becomes <code>final readonly<\/code> and gains an <code>EntityRepositoryInterface<\/code> constructor dependency. In <code>preExport()<\/code>, when the entity being exported is a <code>MenuLinkContentInterface<\/code> and its parent ID starts with <code>menu_link_content:<\/code>, the subscriber extracts the UUID, loads the parent entity via <code>EntityRepositoryInterface::loadEntityByUuid()<\/code>, and calls <code>$event-&gt;metadata-&gt;addDependency()<\/code> to register it. This shim is marked for removal when core issue <a href=\"https:\/\/www.drupal.org\/node\/3562072\">#3562072<\/a> is released.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: February 19, 2026<\/li>\n<li>First commit: February 19, 2026<\/li>\n<li>Last commit: February 27, 2026 (7 days and 4 commits later)<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>phenaproxima (Acquia)<\/li>\n<li>andyg5000 (Dripyard)<\/li>\n<li>vishalkhode (Acquia)<\/li>\n<\/ul>\n<h3>Collaboration<\/h3>\n<p>The issue body credits Andy from Dripyard for reporting the export bugs affecting site template creators. phenaproxima authored and self-merged the fix, noting in the thread that strong test coverage and the need to keep site template work unblocked gave him confidence to merge without external review.<\/p>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3575789: If there's only one site template in the code base, choose it automatically","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3575789.md","guid":"019d857f-33e4-728b-bbba-94a9f4ab6767","pubDate":"Thu, 26 Feb 2026 19:27:11 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Changes:<\/strong> 5 files, +82\/\u221283 \u00b7 php +82\/\u221283<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/8f0de4048273a928e3a1b7d25c054b588bdebb89\">8f0de40<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3575789\">#3575789<\/a> \u00b7 1 contributor \u00b7 6 comments<\/li>\n<li><strong>Followers:<\/strong> 2<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>The Drupal CMS installer now automatically selects a site template if only one is available in the codebase. Previously, the installer always displayed a selection form even when only a single template was present. This streamlines the installation process by skipping the unnecessary selection step. This change prepares for a marketplace workflow where users could download a pre-configured project with a single site template already chosen, then proceed directly through installation without redundant selection prompts.<\/p>\n<h2>Impact<\/h2>\n<p>No upgrade or configuration changes required.<\/p>\n<h2>Technical details<\/h2>\n<p>The <code>drupal_cms_installer_choose_template()<\/code> task was converted from a simple form task to a custom function. This function scans for site templates using <code>RecipeHandler::scan('Site')<\/code> and stores them in install state. If exactly one recipe is found, it temporarily sets <code>$install_state['interactive']<\/code> to FALSE before calling <code>install_get_form()<\/code>, which causes the form to submit programmatically without user interaction.<\/p>\n<p>The <code>RecipeHandler::scan()<\/code> method was refactored from returning a raw Finder object to yielding <code>Recipe<\/code> objects directly, with optional filtering by recipe type. This moves error handling and recipe validation into the service layer. The method now catches <code>RecipeFileException<\/code> and displays errors, simplifying callers.<\/p>\n<p>The <code>SiteTemplateForm<\/code> receives pre-scanned recipes from install state rather than scanning itself. The administrator role recipe is now enqueued earlier in the task function rather than on form submit. The default selection is set to the first recipe in the array, which is always <code>drupal_cms_starter<\/code> due to custom sorting logic.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: February 25, 2026<\/li>\n<li>Committed: February 26, 2026 (1 day later)<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>phenaproxima (Acquia)<\/li>\n<\/ul>\n<h3>Collaboration<\/h3>\n<p>phenaproxima identified the need to support a streamlined marketplace download workflow and implemented the automatic selection behavior within a single day. The change maintains backward compatibility with Drupal CMS's default multi-template setup while enabling future single-template distributions.<\/p>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3573304: Create a 'Blank' template option for users who want to start from scratch","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3573304.md","guid":"019d857f-33e4-728b-bbba-9473cdaec044","pubDate":"Wed, 18 Feb 2026 15:53:41 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Changes:<\/strong> 2 files, +8\/\u22126 \u00b7 php +5\/\u22125 \u00b7 yml +3\/\u22121<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/85790f55005f481e301667998f4d5e251b4afece\">85790f5<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3573304\">#3573304<\/a> \u00b7 4 contributors \u00b7 24 comments<\/li>\n<li><strong>Followers:<\/strong> 4<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>Drupal CMS now offers a &quot;Blank&quot; template option during installation for users who want to start with a minimal foundation. This template provides Drupal CMS's baseline functionality without the Mercury design system or component library, giving developers complete freedom to build their own design approach. The blank template includes an automatically generated minimal theme with just header, content, and footer regions, plus a single blank Canvas page as the home page.<\/p>\n<h2>Impact<\/h2>\n<p>No upgrade or configuration changes required.<\/p>\n<h2>Technical details<\/h2>\n<p>The blank template is implemented by making <code>drupal_cms_site_template_base<\/code> visible and usable during installation. Previously, this site template was hidden from the installer UI and only available to developers via command line.<\/p>\n<p>The implementation leverages the <code>site_template_helper<\/code> Composer plugin to dynamically generate a minimal theme called &quot;blank&quot; when the template is installed. This theme is based on core's <code>starterkit_theme<\/code> but includes only three regions: header, content, and footer. The theme generation happens automatically at <code>composer require<\/code> time through plugin configuration in the site template's <code>composer.json<\/code>.<\/p>\n<p>The <code>drupal_cms_installer_install_tasks_alter()<\/code> function was simplified by removing the <code>SiteTemplateFormAlter<\/code> class that previously hid <code>drupal_cms_site_template_base<\/code> from the installer. The template now appears alongside other options like Starter and the various prebuilt templates.<\/p>\n<p>The installer UI received CSS improvements to accommodate the additional template option, switching from a fixed three-column grid to a responsive grid that adapts from two to four columns based on viewport width. The <code>SiteExporter<\/code> service was updated to remove development-only dependencies like <code>site_template_helper<\/code> when exporting site configurations.<\/p>\n<p>The blank template includes a single empty Canvas page set as the front page to avoid a 404 on first visit. It also includes the utility page content type through <code>drupal_cms_content_type_base<\/code>, which provides the Canvas page functionality.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: February 13, 2026<\/li>\n<li>First commit: February 16, 2026 (3 days later)<\/li>\n<li>Last commit: February 18, 2026 (2 days and 4 commits later)<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>pameeela (Technocrat)<\/li>\n<li>lauriii (Acquia)<\/li>\n<li>phenaproxima (Acquia)<\/li>\n<\/ul>\n<h3>Collaboration<\/h3>\n<p>phenaproxima implemented the technical approach after pameeela identified the need and lauriii provided requirements feedback. phenaproxima resurrected and integrated the <code>site_template_helper<\/code> plugin to enable dynamic theme generation, solving the challenge of scaffolding a minimal theme without maintaining another static theme in the codebase.<\/p>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3574009: Add a hidden Drush command to export all content","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3574009.md","guid":"019d857f-33e4-728b-bbba-948085cf90de","pubDate":"Tue, 17 Feb 2026 15:16:32 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Changes:<\/strong> 6 files, +138\/\u221235 \u00b7 php +138\/\u221235<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/8a6b09bacd4a25dca24ba392ea9a63754131c5c7\">8a6b09b<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3574009\">#3574009<\/a> \u00b7 1 contributor \u00b7 6 comments<\/li>\n<li><strong>Followers:<\/strong> 2<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>Drupal CMS now includes a hidden Drush command that makes it easy for developers to export all content from a site to a directory. This is useful for creating development snapshots, migrating content between environments, or backing up site content in a portable format. The command is hidden because it's intended for developer use rather than routine site administration. The change also extracts the content loading logic into a reusable component that both the site export and content export commands can use.<\/p>\n<h2>Impact<\/h2>\n<p>No upgrade or configuration changes required.<\/p>\n<h2>Technical details<\/h2>\n<p>This change adds a new <code>content:export:all<\/code> Drush command in the <code>drupal_cms_helper<\/code> module. The command is marked as hidden (not shown in normal help listings) and exports all content entities to a specified directory using Drupal's default content export system.<\/p>\n<p>The implementation extracts the content loading logic from <code>SiteExporter::loadAllContent()<\/code> into a new <code>ContentLoader<\/code> class that implements <code>IteratorAggregate<\/code>. The <code>ContentLoader<\/code> iterates through all entity types, filters out internal entities and path aliases, and yields all content entities. It excludes users 0 and 1 since those always exist with the same IDs.<\/p>\n<p>The new <code>ExportAllContentCommand<\/code> class uses the <code>Exporter<\/code> service to export each entity with its dependencies to the target directory. The command accepts a single required argument for the destination directory path.<\/p>\n<p>Both <code>ContentLoader<\/code> and <code>ExportAllContentCommand<\/code> are marked as <code>@internal<\/code>, but the command itself is marked as <code>@api<\/code> to signal that developers can rely on it. The change also adds <code>@api<\/code> annotations to existing commands (<code>content:import<\/code>, <code>site:export<\/code>) to clarify their stability guarantees within Drupal CMS.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: February 17, 2026<\/li>\n<li>Committed: February 17, 2026<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>phenaproxima (Acquia)<\/li>\n<\/ul>\n<h3>Collaboration<\/h3>\n<p>phenaproxima identified the use case and implemented the feature in a single day, including extracting the shared content loading logic into a reusable component and updating the existing site export functionality to use it.<\/p>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3569529: Add an AGENTS.md file","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3569529.md","guid":"019d857f-33e3-726a-abae-0f8241a88f1c","pubDate":"Wed, 04 Feb 2026 17:24:31 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal feature request<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Changes:<\/strong> 5 files, +134\/\u22120 \u00b7 md +134<\/li>\n<li><strong>Diff:<\/strong> <a href=\"https:\/\/git.drupalcode.org\/project\/drupal_cms\/-\/commit\/aecaffbe229e6296ba2092a2bc073b2f2b7807dc\">aecaffb<\/a><\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3569529\">#3569529<\/a> \u00b7 4 contributors \u00b7 12 comments<\/li>\n<li><strong>Followers:<\/strong> 13<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>Drupal CMS now includes an AGENTS.md file to help AI coding assistants work more effectively with the codebase. This file provides structured guidance on the project's technology stack, coding standards, development commands, and prohibited actions. The documentation helps AI tools avoid common mistakes and follow project conventions when assisting developers with Drupal CMS contributions.<\/p>\n<h2>Impact<\/h2>\n<p>No upgrade or configuration changes required.<\/p>\n<h2>Technical details<\/h2>\n<p>The new <code>AGENTS.md<\/code> file documents Drupal CMS-specific conventions for AI coding assistants. It covers the monorepo structure with recipes in the <code>recipes<\/code> directory, the <code>drupal_cms_helper<\/code> module, and the <code>project_template<\/code> directory used for end-user installations.<\/p>\n<p>The file provides command references for common development tasks using DDEV and Drush, including recipe application, PHPUnit testing, site installation, and content export with <code>drush content:export<\/code>. It specifies coding standards like using strict typing, the <code>?type<\/code> nullable syntax, <code>#[\\Override]<\/code> attributes, and marking new classes as final with private members.<\/p>\n<p>Several README files in protected directories (<code>cpanel_template<\/code>, <code>patches<\/code>, <code>scripts<\/code>, <code>splitter<\/code>) now explicitly warn agents not to modify those areas. The prohibited actions section lists destructive operations like running <code>ddev tag<\/code> or <code>composer update<\/code> outside the project root, and prevents agents from adding patches or pinning dependencies in Composer files.<\/p>\n<p>The documentation emphasizes defensive programming in <code>drupal_cms_helper<\/code>, requiring checks like <code>\\Drupal::moduleHandler()-&gt;moduleExists()<\/code> before using optional module functionality. It also instructs agents to mark new non-test classes as internal with specific warning text.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: January 25, 2026<\/li>\n<li>Committed: February 4, 2026 (9 days and 2 commits later)<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>phenaproxima (Acquia)<\/li>\n<li>nod_ (Tr\u00e8s Bien Tech)<\/li>\n<li>marcus_johansson (FreelyGive)<\/li>\n<li>thejimbirch (Kanopi Studios)<\/li>\n<li>rajab natshah (Vardot)<\/li>\n<\/ul>\n<h3>Collaboration<\/h3>\n<p>This issue demonstrates the Drupal CMS team's pragmatic approach to AI tooling. The issue author acknowledged ambivalence about AI while recognizing its practical utility and the need to accommodate developers using these tools. Multiple contributors provided feedback to improve structure and readability, with discussion about whether to manage the file through a separate package like the scaffold system. The team decided keeping <code>AGENTS.md<\/code> in the repository made more sense for documenting internal workings.<\/p>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"},{"title":"#3557383: Ship local video media type with Drupal CMS for better compatibility with Canvas","link":"https:\/\/github.com\/dbuytaert\/drupal-digests\/blob\/main\/issues\/drupal-cms\/3557383.md","guid":"019dcd57-1279-7774-a274-b91262fab190","pubDate":"Wed, 19 Nov 2025 07:46:39 +0000","description":"<ul>\n<li><strong>Project:<\/strong> Drupal CMS<\/li>\n<li><strong>Type:<\/strong> Normal task<\/li>\n<li><strong>Status:<\/strong> Fixed<\/li>\n<li><strong>Discussion:<\/strong> <a href=\"https:\/\/www.drupal.org\/node\/3557383\">#3557383<\/a> \u00b7 3 contributors \u00b7 15 comments<\/li>\n<li><strong>Followers:<\/strong> 5<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>The <code>drupal_cms_remote_video<\/code> recipe now also ships the local video media type, so Canvas components that reference a <code>video<\/code> schema can use the media library picker instead of falling back to a less reliable file picker. The recipe description has been updated to reflect that it now covers both locally and remotely hosted video. Additionally, whitespace-to-underscore filename sanitization is enabled for uploaded files, and the built-in <code>files<\/code> view is disabled to avoid confusing site editors who may not understand the difference between &quot;Files&quot; and &quot;Media&quot;.<\/p>\n<h2>Impact<\/h2>\n<p>No upgrade or configuration changes required.<\/p>\n<h2>Technical details<\/h2>\n<p>The root problem was that Canvas resolves a <code>video<\/code> field (via <code>json-schema-definitions:\/\/canvas.module\/video<\/code>) through the local video media type. When that media type was absent, Canvas degraded to a generic file picker that was unreliable. The fix adds <code>core\/recipes\/local_video_media_type<\/code> to the <code>recipes<\/code> list in <code>drupal_cms_remote_video\/recipe.yml<\/code>, alongside the existing <code>core\/recipes\/remote_video_media_type<\/code>. The recipe also installs the <code>file<\/code> and <code>views<\/code> modules explicitly. A new <code>core.entity_form_display.media.video.*<\/code> config action mirrors the existing remote video form display: it hides <code>uid<\/code>, <code>langcode<\/code>, <code>path<\/code>, and <code>created<\/code>, and enables the <code>name<\/code> field. <code>file.settings<\/code> is updated via <code>simpleConfigUpdate<\/code> to set <code>filename_sanitization.replace_whitespace: true<\/code> in both <code>drupal_cms_remote_video<\/code> and <code>drupal_cms_media<\/code> (the latter backported to the 1.2.x branch of <code>drupal_cms_image<\/code> as a bug fix). <code>views.view.files<\/code> is disabled, and authenticated users receive the <code>delete own files<\/code> permission. The <code>composer.json<\/code> description was updated from &quot;Configures display options for remote video&quot; to &quot;Configures display options for locally and remotely hosted video.&quot; A follow-up is planned to explore a Composer package alias so the recipe can also be referenced as <code>drupal\/drupal_cms_local_video<\/code>, since the current package name (<code>drupal_cms_remote_video<\/code>) no longer reflects its full scope.<\/p>\n<h2>Contribution<\/h2>\n<h3>Timeline<\/h3>\n<ul>\n<li>Opened: November 12, 2025<\/li>\n<li>Committed: November 19, 2025 (6 days and 3 commits later)<\/li>\n<\/ul>\n<h3>Key contributors<\/h3>\n<ul>\n<li>mherchel<\/li>\n<li>pameeela (Technocrat)<\/li>\n<li>phenaproxima (Acquia)<\/li>\n<\/ul>\n<h3>Collaboration<\/h3>\n<p>pameeela noted early in the thread that adding the local video media type had always been planned as optional support, but she saw no major reason to keep it optional and suggested simply depending on the core recipe. phenaproxima agreed, opened MR !685, and then flagged the awkward naming of <code>drupal_cms_remote_video<\/code> after the fact. pameeela acknowledged the naming issue but approved the change rather than holding it up, reasoning it was not worth blocking the fix.<\/p>\n<hr \/>\n<p><em>This content is AI-generated and may contain errors. See <a href=\"https:\/\/github.com\/dbuytaert\/drupal-digests\/\">Drupal Digests<\/a> for more.<\/em><\/p>\n"}]}}