Guide

Drupal blocks rearranged unexpectedly — detecting bulk block placement changes

You open the homepage and the right sidebar is gone. The newsletter block has moved from sidebarfirst to footerfourth. Three CTA blocks that were under the hero are now stacked at the bottom of the page, below the comments.

1. Problem

You open the homepage and the right sidebar is gone. The newsletter block has moved from sidebar_first to footer_fourth. Three CTA blocks that were under the hero are now stacked at the bottom of the page, below the comments. The header search disappeared. The "Featured products" block is in the wrong region on every node page. Nobody filed a deploy ticket. Nobody touched config in Git. Editors are pinging you on Slack with "drupal blocks moved without me," "drupal sidebar widgets disappeared," "drupal block placement audit who changed" — and /admin/structure/block shows a layout you don't recognise.

This usually surfaces as a burst of drupal_block_change_total increments inside a 60-second window — six, ten, sometimes thirty block placement events from a single session, each one moving a block between regions or removing it from a theme entirely. The watchdog log records each one as a generic config save. The UI shows the new state. The old layout is gone. There is no "who" anywhere on the screen the editor is staring at.

This guide is about catching that burst the moment it happens, decomposing it by region + block_type + operation, correlating it with drupal_blocks_by_theme distribution shifts, and rolling back from block_content revisions or Layout Builder history before the rearranged sidebar starts costing you ad impressions.

2. Impact

Blocks are not decoration in Drupal. They are the information architecture. The "Subscribe" block in sidebar_first is the newsletter funnel. The "Buy now" block in content_top is the conversion path. The header search is how 35% of mobile users navigate. The footer block with legal links is what keeps the site GDPR-compliant. When a block moves region — or disappears entirely — the page still renders 200 OK. Uptime says fine. The funnel is broken.

The realistic damage:

  • An ad block that earns $4–8 per thousand impressions is moved from sidebar_first (visible on 80% of pages) to footer_fifth (below the fold on long article pages only). Impressions collapse 70%. At 400k pageviews/month, that is ~$1,500–3,000 of monthly revenue gone, invisible to GA until the end-of-month report.
  • A "Featured collection" block on the storefront is removed from the active theme during a bulk save. The product was generating 12% of add-to-carts. Conversion drops 9% the same day. Marketing blames "a slow week."
  • An attacker with a compromised editor account places their own block_content (with malicious JS or affiliate injection) into header on every theme, then removes the legitimate logo block to mask the swap. Logged-in admins on the admin theme see normal; anonymous traffic sees broken-but-monetised.
  • A staging-to-production deploy that flips system.theme.default triggers a cascade of placement changes as Drupal re-resolves region assignments. Sixty events in 90 seconds, none annotated as "this was a theme switch."
  • An editor mistakenly drags "Recent comments" out of the sidebar. The block is now in _disabled. Community engagement flatlines because the block is gone, not because comments stopped.

A bulk burst is also one of the cleanest fingerprints of an account takeover. Real editors edit one block, save, look at the page, edit the next. Twelve saves in 40 seconds is not human pacing.

3. Why It’s Hard to Spot

Drupal does not have a native "block layout audit log." /admin/structure/block shows the current state. No diff, no history tab, no "modified by" column. The watchdog channel treats placement changes as routine config writes — they hit dblog as a generic Configuration was synchronised line, indistinguishable from a hundred other config saves a busy site emits per day.

Layout Builder keeps revisions of node-level layouts but not of the global block layout for system.theme..block. placement. The block.block. config entity records the final state. The previous state lives only in your config export — if you export, and between* the bad save and the next legitimate save.

Uptime monitors miss it completely. The page renders 200. Pingdom doesn't know what the right blocks are. Editors notice — but only the ones who look at the affected pages, and only if they remember what was there yesterday. By the time three editors complain on Slack, the burst is hours old, the actor's session has expired, and the only forensic trail is dblog rows you have to manually correlate by timestamp.

The deepest source of confusion is the theme dimension. Most production Drupal sites have at least two themes — a default theme for visitors and an admin theme for /admin/*. A block placement change in one theme leaves the other untouched. An admin browsing in the admin theme sees the layout they expect; the visitor theme is broken. This is why "drupal sidebar widgets disappeared" reports often come from anonymous users while internal QA cannot reproduce.

4. Cause

drupal_block_change_total is a counter the processor increments every time a block.change event arrives from the Logystera Drupal module. The module hooks hook_block_presave/insert/update/delete, plus the ConfigCrudEvent for block.block. config writes (where the placement* — region + theme + weight — is actually stored).

Each block.change event captures:

  • Actoruid and name from the session at the moment of save (or 0/anonymous for CLI/Drush imports).
  • Block ID — the block instance ID, e.g. olivero_search, claro_breadcrumbs, bartik_views_block__articles_block_1.
  • Block type — content (block_content), system (system_branding_block), views (views_block), menu (system_menu_block), custom plugin.
  • Theme — which theme the placement applies to (olivero, claro, umami, gin).
  • Region — destination region (sidebar_first, content, footer_fourth, _disabled).
  • Operationplace, remove, reorder (weight change within the same region), update_visibility, disable.
  • Originating request — usually a POST /admin/structure/block/manage/ or POST /admin/structure/block (the bulk save endpoint), or a drush config:import with no HTTP context.

The metric's labels are entity_id, entity_type, theme, block_type, operation. A normal day looks like one or two placements per editor, spread across hours. A burst looks like fifteen operation=place and eight operation=remove increments on the same theme inside 60 seconds. The processor's drupal_block_burst_detect rule fires when rate(drupal_block_change_total{theme=~".+"}[1m]) > 6 — the six-block-changes-in-one-minute threshold, empirically the line between human pacing and "something is happening that shouldn't be."

drupal_blocks_by_theme is the gauge that backs the burst — the count of currently-placed blocks per theme. A theme switch, bulk remove, or Layout Builder import shifts the distribution: drupal_blocks_by_theme{theme="olivero"} drops from 24 to 11 over 90 seconds while theme="claro" is flat. That delta proves the burst is real and scoped, not a counter spike from a noisy module.

5. Solution

Work outward from the burst. Start with the metric, then the watchdog rows, then the actor and the originating request, then the rollback path.

5.1 Diagnose

The first move is to confirm the burst is a real burst and not a Layout Builder migration or a planned theme switch.

# Pull block.change events from the last hour, ordered by time
drush sql:query "SELECT timestamp, uid, message, variables FROM watchdog \
  WHERE type = 'block' AND timestamp > UNIX_TIMESTAMP() - 3600 \
  ORDER BY timestamp DESC LIMIT 100;"
# → surfaces drupal_block_change_total events with actor uid attached

Filter to a one-minute window around the suspected burst. If editors say "the sidebar disappeared around 14:20," that is the time correlation anchor — exactly the kind of timestamp that lets you separate a routine edit from a bulk save:

# Find block changes in the 60s window after a deploy or off-hours boundary
drush sql:query "SELECT FROM_UNIXTIME(timestamp) AS t, uid, message FROM watchdog \
  WHERE type IN ('block','config') AND timestamp BETWEEN UNIX_TIMESTAMP('2026-04-27 14:19:00') \
  AND UNIX_TIMESTAMP('2026-04-27 14:21:00') ORDER BY timestamp;"
# → if you see 8+ rows in 60s, drupal_block_change_total is firing as a burst
# Cross-reference with admin requests for the same actor — drupal_admin_requests_total
tail -5000 /var/log/nginx/access.log | grep "POST /admin/structure/block" | awk '{print $1, $4, $7}'
# → ties drupal_block_change_total to the originating session and IP
# Diff the current block layout against the last config export
drush config:export --destination=/tmp/now
diff -ru /var/www/config/sync/block.block.*.yml /tmp/now/block.block.*.yml | head -200
# → shows exactly which placements moved (region, weight, theme) per block

The time-correlation step is the one that turns a list of saves into a story. A burst at 02:14 UTC on a Sunday is not an editor. A burst at 09:02 immediately after a drush deploy is a config import you forgot to coordinate. A burst at 14:18 fifteen seconds after auth.login_success from an IP not in your allow-list is a takeover.

Pair the watchdog query with the metric panel: drupal_block_change_total should show a near-vertical step in the same minute. If drupal_blocks_by_theme{theme="olivero"} also drops at the same instant, the burst included operation=remove. If only drupal_block_change_total spiked but drupal_blocks_by_theme is flat, the burst was reorders within existing regions — usually less destructive but still worth logging the actor.

5.2 Root Causes

Mapped to which signal each produces and how it appears in logs:

  • Theme switch rippledrupal_block_change_total spike with operation=place and operation=remove in equal counts, scoped to one theme. Watchdog shows system.theme config write immediately preceding. drupal_blocks_by_theme distribution shifts cleanly between two themes.
  • Layout Builder bulk importdrupal_block_change_total burst with no auth.login_success in the preceding 5 minutes; originating request is drush config:import. drupal_structure_changes_total increments alongside.
  • Editor bulk drag-and-drop on /admin/structure/block → 6–12 drupal_block_change_total events in 30 seconds, all from one uid, mostly operation=reorder. drupal_admin_requests_total{path="/admin/structure/block"} shows a single POST for the whole save.
  • Editor mistake (block dragged to _disabled) → single operation=remove event with region=_disabled, drupal_blocks_by_theme decrements by 1.
  • Compromised account / takeover → burst from a user that hasn't logged in for weeks, paired with block_content create events and auth.login_success from a new IP/geo immediately preceding.
  • Module update placing default blocks → burst where actor.uid=0 or actor.name=cli from drush updatedb. drupal_structure_changes_total increments with source=update_hook.

5.3 Fix

Step-by-step, prioritized:

  1. Identify the actor and freeze the session. Pull the uid from the burst's first watchdog row. Force a logout: drush user:cancel-block . Reset the password and require MFA on next login.
  2. Diff active config against the last known good export. Run drush config:export to a scratch directory, then diff against config/sync/. Every changed block.block.*.yml is a placement that moved.
  3. Roll back placement via config import. If config/sync/ represents the desired state, drush config:import reverts every placement change in one command. For partial reverts, copy individual block.block..yml files into config/sync/ and run drush config:import --partial.
  4. Roll back block_content body edits via revisions. Custom blocks have revision history (block_content_revision table). Use drush eval to load the entity, list revisions, and revert via setNewRevision(false); save();. Layout Builder layouts have node-level revision rollback via /node//revisions.
  5. Restore from the last good drush deploy artifact. If the burst spans more than blocks (full theme corruption, module list changes), drush config:import from your release artifact is safer than hand-picking files.
  6. Audit the actor's other actions. For takeover cases, check drupal_structure_changes_total, role/permission changes (user.role.*), and block_content creates from the same uid in the prior 24 hours. A bad actor rarely changes only blocks.
  7. Lock down administer blocks. In production, no role except Site Architect should have administer blocks and administer block_content. Editors should use Layout Builder per-page, which is revisioned and scoped.

5.4 Verify

After the rollback, two things must happen for the issue to be considered resolved:

  1. Signal disappearance: drupal_block_change_total rate must drop to baseline. No new increments in the next 30 minutes under normal admin traffic. Watch the panel.
  2. Expected baseline: A healthy production Drupal site emits 0–3 drupal_block_change_total events per hour during business hours, almost always from known editor accounts on the administer blocks allowlist, and 0 per hour outside business hours unless a deploy is running. If you're still seeing 5+/hour with no deploy or active editor session, the underlying cause is not resolved — likely a script with stored credentials still running, or a scheduled drush job nobody documented.

Confirm drupal_blocks_by_theme{theme="olivero"} (or your default theme) has returned to its pre-burst value. If the count is one or two off, find the missing block ID by diffing drush config:list --filter=block.block before and after.

A flat drupal_block_change_total line with the editor still logged in and viewing the block layout page is the healthy steady state. A flat line with drupal_blocks_by_theme matching the pre-incident value is "fixed." A flat line with drupal_blocks_by_theme still off by 3 means a removal wasn't reverted — keep diffing.

6. How to Catch This Early

Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.

This issue surfaces as drupal_block_change_total.

Everything you just did manually — pull watchdog rows, correlate by timestamp, diff config exports, identify the actor — Logystera does automatically. The same drupal_block_change_total you just queried is detected, charted by theme + block_type + operation, and alerted in real time when the per-minute rate crosses six events.

1. The signal in the dashboard.

!Logystera dashboard — drupal_block_change_total over time

drupal_block_change_total burst at 14:18, six placements in 40 seconds, immediately after editor login from a new IP.

2. The alert that fires.

!Logystera alert — Drupal block placement burst detected

Critical alert fires within 60s of the sixth block placement event, with actor uid, theme, and the watchdog rows attached as evidence.

3. Why this matters.

The fix is simple once you know the problem. The hard part is knowing it happened at all. A site can lose 70% of ad impressions, a third of newsletter signups, or its entire conversion funnel because a sidebar block moved — and the only person who notices is a customer who emails support 48 hours later. Logystera turns this kind of failure from a Slack-screenshot emergency into a 60-second notification with the actor, the theme, the affected blocks, and a one-click link to the watchdog evidence that proves it.

7. Related Silent Failures

  • drupal_structure_changes_total — bulk view, menu, or taxonomy changes from the same actor, often paired with block bursts during account takeovers. See: Drupal config changing without anyone touching it.
  • views.change — view modifications that change which content appears in a block (a block might be in the right region but rendering the wrong query). See: Drupal views being modified.
  • drupal_admin_requests_total — anomalous admin traffic that precedes structural changes. See: Drupal admin path access pattern detection.
  • auth.login_success from new geo/IP — the login that immediately precedes a block burst is the one that matters most. See: Drupal account takeover via brute force user login.
  • block_content revision rollbacks — when the placement is fine but the block body has been swapped (logo replaced, CTA text changed). See: Drupal block content body modified — content integrity tracking.

See what's actually happening in your Drupal system

Connect your site. Logystera starts monitoring within minutes.

Logystera Logystera
Monitoring for WordPress and Drupal sites. Install a plugin or module to catch silent failures — cron stalls, failed emails, login attacks, PHP errors — before users report them.
Company
Copyright © 2026 Logystera. All rights reserved.