Drupal Site Manager
FreeOfficialManage Drupal in all popular forms: drush CLI, REST/JSON:API, module management, content types, Views, performance caching, and security hardening.
Model requirements
standard
16k tokens
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/webin 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
--uriand by JSON:API clients - Headless or traditional? — determines JSON:API/REST vs. drush content ops
- Configuration sync directory — the
config/syncdir 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:importoverwrites it; always export toconfig/syncand commit to Git before deploying. - Running DB updates out of order — after
composer update, always rundrush updatedbbeforeconfig: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_codefrom all non-admin roles. - JSON:API with write enabled and no auth —
read_only: falsewithout 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.