Drupal Site Manager

FreeOfficial

Manage Drupal in all popular forms: drush CLI, REST/JSON:API, module management, content types, Views, performance caching, and security hardening.

operationsdrupaldrushcmsheadless-cmsjson-apioperationsviewssecurity· v1· by SkillingMain
63
Usefulness score
837
Installs
No ratings yet
today
Last updated

Model requirements

Capability tier

standard

Min context window

16k tokens

Recommended models
Claude Haiku 3.5GPT-4o-miniGemini 2.0 FlashLlama 3.1 8B (self-hosted)Qwen 2.5 14B (self-hosted)Phi-4 (self-hosted)

Skill instructions

When to use

Use this skill when managing a Drupal site in any form: a self-hosted install operated via drush, a headless Drupal backend consumed by Next.js/Gatsby via JSON:API or REST, a multisite installation, or a complex content model with custom content types and Views. It covers module and theme lifecycle, content type and field design, Views configuration, Twig template management, caching strategy, security hardening, backup/restore, and headless-CMS integration. Reach for it for batch content operations, automated deployments, configuration management, security audits, and migrations where clicking through the admin UI is impractical or error-prone.

Inputs to gather

  • Drupal docroot path — the web root containing index.php (typically /web in a Composer project)
  • Drupal version — 10.x or 11.x (drush commands differ slightly); confirm with drush status
  • Drush version — need 12+ for Drupal 10/11; confirm with drush --version
  • SSH access — required for drush on remote hosts
  • Composer-based or tarball install — modern Drupal is Composer-managed; config differs
  • Site URI / domain — used by drush for --uri and by JSON:API clients
  • Headless or traditional? — determines JSON:API/REST vs. drush content ops
  • Configuration sync directory — the config/sync dir for CMI (Configuration Management Initiative) exports

Procedure

0. Verify drush and the install

drush status                      # version, URI, DB connection, Drupal root, PHP version
drush core:requirements           # check the status report for errors
drush pm:list --status=enabled    # all enabled modules + versions
drush cron:run                    # run scheduled tasks; verify no errors
drush updatedb:status             # pending DB schema updates?
drush cache:rebuild               # clear all caches (Drupal's caches are deep)

1. Configuration management (CMI) — the source of truth

Drupal stores config in YAML; export/import it so config lives in Git, not just the DB:

# Export current config to config/sync
drush config:export -y
# Commit config/sync/*.yml to Git

# Apply config from Git to the DB (after pulling changes)
drush config:import -ync

# See what config would change before importing
drush config:status               # shows Added/Changed/Deleted

Never make config changes directly in the DB without exporting — the next config:import will overwrite them. Always: change in admin UI → config:export → commit → deploy → config:import.

2. Core, module, and theme management

# Check for available updates
drush pm:updatestatus

# Update modules and run DB updates (Composer-based install)
composer update drupal/* --with-all-dependencies
drush updatedb -y
drush cache:rebuild

# Enable/disable modules
drush pm:enable pathauto metatag jsonapi simple_sitemap -y
drush pm:disable statistics -y
drush pm:uninstall statistics -y   # fully uninstall (removes data + schema)

# Install a new module via Composer (correct way for modern Drupal)
composer require drupal/address

3. Content type and field design

# List content types and their fields
drush php:eval '
$types = \Drupal::entityTypeManager()->getStorage("node_type")->loadMultiple();
foreach ($types as $t) { echo $t->id().": ".$t->label()."\n"; }'

drush field:info node article      # all fields on the article bundle

# Create a content type with drush (or use the admin UI + config export)
drush php:script <<'PHP'
\Drupal::entityTypeManager()->getStorage('node_type')->create([
  'type' => 'event',
  'name' => 'Event',
  'description' => 'An upcoming event.',
])->save();
PHP

# Add a field via config (preferred — lives in Git)
# Export the field config, edit config/sync/field.field.node.event.field_date.yml, then:
drush config:import -y
drush cache:rebuild

4. Content operations

# List nodes
drush node:list --type=article --status=1 --limit=50

# Create a node
drush php:script <<'PHP'
\Drupal::entityTypeManager()->getStorage('node')->create([
  'type'  => 'article',
  'title' => 'Published via Drush',
  'status' => 1,
  'body'  => ['value' => '<p>Body content</p>', 'format' => 'full_html'],
])->save();
PHP

# Bulk update: unpublish all nodes of a type older than 90 days
drush php:eval '
$nids = \Drupal::entityQuery("node")
  ->condition("type", "article")
  ->condition("created", strtotime("-90 days"), "<")
  ->accessCheck(FALSE)->execute();
$nodes = \Drupal::entityTypeManager()->getStorage("node")->loadMultiple($nids);
foreach ($nodes as $n) { $n->set("status", 0)->save(); }
echo "Unpublished " . count($nodes) . " nodes.";
'

# Delete all nodes of a type
drush node:delete --type=legacy_content -y

5. Views configuration

Views are config entities — manage them via CMI, not the DB:

# Export current Views config
drush config:export -y
# Edit config/sync/views.view.featured_articles.yml in your editor
# Add a display, filter, or relationship, then import:
drush config:import -y
drush cache:rebuild

To programmatically create or modify a View, use the Views module's PHP API, export it to config, and commit. Never hand-edit a live View in the DB without exporting — the next deploy's config:import will revert it.

6. JSON:API — headless Drupal integration

Enable the JSON:API module (core, disabled by default) and configure permissions:

drush pm:enable jsonapi jsonapi_extras -y
drush config:set jsonapi.settings read_only false -y   # allow writes if the frontend creates content
drush cache:rebuild

Query the API (no auth needed for public content by default):

# List articles with field filtering and sparse fieldsets
curl -s "https://drupal.example.com/jsonapi/node/article?\
page[limit]=10&\
fields[node--article]=title,field_summary,created&\
sort=-created" | jq '.data[].attributes.title'

# Fetch related author (included) in one request
curl -s "https://drupal.example.com/jsonapi/node/article?\
include=uid&fields[user--user]=name,mail"

# Create a node (requires OAuth or Basic Auth + permissions)
USER=admin; PASS="$(drush user-password admin --password='strong')"
curl -u "$USER:$PASS" -X POST https://drupal.example.com/jsonapi/node/article \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "node--article",
      "attributes": {
        "title": "Created via JSON:API",
        "body": { "value": "<p>From the API</p>", "format": "full_html" }
      }
    }
  }'

For a headless Next.js frontend, use the next-drupal library which handles JSON:API fetch, auth, and preview. For REST (alternative to JSON:API): drush pm:enable rest restui, then configure resources at /admin/config/services/rest. For GraphQL: composer require drupal/graphql.

7. Twig template management

Twig templates live in the theme; never edit core templates in place — always override in a sub-theme:

# Copy a core template to your theme for override
cp web/core/themes/classy/templates/field/field.html.twig \
   web/themes/custom/mytheme/templates/field/field--node--article.html.twig

# Rebuild the theme registry after adding templates
drush cache:rebuild

# Enable Twig debug mode (development only) to see template suggestions
drush config:set system.performance twig_debug true -y   # or edit services.yml
drush cache:rebuild

Turn off Twig debug and aggregation off in production via system.performance config.

8. Caching strategy

Drupal has a sophisticated cache layer. Tune it for production:

# Set production cache settings via config
drush config:set system.performance cache.page.max_age 3600 -y     # page cache TTL
drush config:set system.performance css.preprocess true -y        # aggregate CSS
drush config:set system.performance js.preprocess true -y         # aggregate JS

# Enable the dynamic page cache module (core, big perf win for authed users)
drush pm:enable dynamic_page_cache page_cache -y

# For high-traffic sites, enable Redis as the cache backend
composer require drupal/redis
drush config:set redis.connection.host redis -y
drush config:set redis.connection.port 6379 -y
# Set cache backend in settings.php: $settings['cache']['default'] = 'cache.backend.redis';

drush cache:rebuild

9. Security hardening

# Remove the default user 1 predictable username? No — but ensure a strong password
drush user-password admin --password="$(openssl rand -base64 24)"

# Review all user roles and permissions
drush role:list
drush php:eval '
foreach (\Drupal::entityTypeManager()->getStorage("user_role")->loadMultiple() as $role) {
  echo $role->id().": ".implode(",", array_keys($role->getPermissions()))."\n";
}'

# Remove dangerous permissions from non-admin roles (PHP filter, admin menus)
drush php:eval '
$role = \Drupal::entityTypeManager()->getStorage("user_role")->load("editor");
$role->revokePermission("use text format php_code")->trustData()->save();
'

# Enforce HTTPS (via .htaccess or settings.php)
drush config:set system.performance cache.page.max_age 3600 -y
# Add to settings.php: $config["system.logging"]["error_level"] = "hide";

# Run security review via the security_review module
drush pm:enable security_review -y
drush security:run

10. Backup and restore

# Full backup: DB + files + code
drush sql:dump --result-file=db-$(date +%F).sql --gzip
tar czf files-$(date +%F).tar.gz web/sites/default/files

# Archive config too (it's the source of truth)
cp -r config/sync config-backup-$(date +%F)

# Restore
drush sql:query --file=db-2024-06-01.sql

11. Multisite management

# List all sites in a multisite install
drush site:alias | grep -v '@self'

# Run a command across all sites
drush @sites pm:enable pathauto -y
drush @sites updatedb -y
drush @sites cache:rebuild

Output format

A managed Drupal site: configuration version-controlled in Git via CMI, modules/themes updated with DB updates applied, content types and fields designed and exported as config, Views configured in code, JSON:API enabled and tested for headless consumption, Twig templates overridden safely in a sub-theme, caching tuned for production (dynamic page cache + optional Redis), security hardened (strong admin password, dangerous permissions revoked, security review passed), and a full backup (DB + files + config) stored off-site. A report of enabled modules, roles/permissions, PHP version, and cache configuration.

Common pitfalls

  • Editing config in the DB without exporting — the next config:import overwrites it; always export to config/sync and commit to Git before deploying.
  • Running DB updates out of order — after composer update, always run drush updatedb before config:import; schema and config changes are coupled.
  • Forgetting drush cache:rebuild — Drupal caches deeply (routes, theme registry, render arrays); changes won't appear until caches are rebuilt.
  • Hand-editing a live View in the admin UI — it lives in config; export it or it reverts on deploy. Use the UI to configure, then export.
  • Enabling the PHP filter module in production — it allows arbitrary PHP execution from content; remove use text format php_code from all non-admin roles.
  • JSON:API with write enabled and no authread_only: false without authentication lets anyone create/delete content; secure it with OAuth or Basic Auth.
  • Twig debug mode left on in production — it leaks template suggestions in HTML comments and disables caching; disable via system.performance.
  • Ignoring drush core:requirements — it surfaces DB errors, missing modules, and cron failures that silently degrade the site.