Skip to main content
Home
term:~# less tar.gz.ro
Read-only. Think freely.

Main navigation

  • Home
    • My Yard Projects
  • AIX World
    • AIX Commands
    • HACMP Commands
    • HMC Commands
    • How to assign disks to AIX partition via VIO
    • Misc stuff
    • NIM (Network Installation Management)
    • Virtual I/O (VIO)
  • Apple World
    • News
    • Tips & Tricks
    • iOS
  • CygWin World
    • CygWin Tips and Tricks
    • Cygwin: clone disk with dd
    • Cygwin: how to have multiple tabs in Cygwin's Mintty
    • Cygwin: start and enable sshd
    • config.guess: unable to guess system type
  • ESXi World
    • ESXi 7: Create VMFS Partition Via Command Line
    • ESXi : A bunch of useful commands and information
    • ESXi: How to change the hostname of an ESXi host
    • ESXi: how to change the MTU of vmkernek (vmk) interface (when UI fails)
    • ESXi: how to resize the OS partition
    • How to create a bootable USB installer of ESXi 7.0
  • Games
    • Assetto Corsa Rally vs Real Life – La Bollène-VĂ©subie → Col de Turini | Monte-Carlo Rally
    • Assetto Corsa Rally – Early Access Deep Dive
    • Diablo 3 & Reaper of Souls tips
    • EA Sports WRC Time Trial Locations and Stages
    • EA Sports WRC vs Assetto Corsa Rally | Visual Comparison (Gravel, Asphalt, Wet, Night, Snow)
    • Global Chip Shortage? Build this oddity: PC From 2009 With GeForce GTX 1050ti 4G
    • Left4Dead Lan Play
    • Live Sessions
    • Mafia II Joe's Adventures - Playboy Locations
  • HP-UX World
    • Boot into HPUX L2000
    • How to increase the size of a filesystem in LVM
    • Miscellaneous
    • Performance tools
    • Working with HP VM
  • Hardware
    • 5 Months with iPhone 15 Pro: Not PERFECT But... Must See. From Gaming to ProRes Requirements.
    • Apple Watch Series 9? Not Yet! I Still Love My "Seven of Nine" 19 months later
    • Asus ROG Sheath in 2024: a HUGE Mousepad, Bigger Than Logitech G840 XL
    • Choose Your Perfect Mic / Lavalier | AIQIU M31 vs. Boya BY-M3/BY-M2 & Puluz PU425
    • DJI Mic Mini 2 – Real-World Review and Testing
    • How I Test Mousepads: Glide, Control, and Surface Noise Explained
    • How to build a 25U 600x600 Server Rack
    • I Tested Creative Pebble V2 on My Sim Rig – Surprising Results!
    • Icy Box IB-3805-C31 speed test
    • Is the RX 9060 XT Mercury Class Worth It? Silent Power Meets Real Gameplay
    • Logitech Pro X Wired Gaming Headset (2nd gen): review + mic test with G HUB on Windows 11 and macOS
    • Logitech RS H-Shifter - Amazing Feel, But There’s a Catch (As Always)
    • Mousepad Logitech G840
    • PX8 S2 – A Journey Through Sound, Expectations and Reality
    • Play Steam Games on iPhone with Backbone One Playstation Edition gen2 USB-C / Steam Deck Alternative
    • Qnap TR-004 short review - working with linux
    • Razer Gigantus V2 XXL – 5 Months Later (Still Worth It?)
    • Samsung 870 QVO 2TB: Speed Tests and comparison with Adata SU650 and Kingston K600
    • Server-Ready MS-01 - A Small PC with Everything: SFP+, USB4, 2.5gbps LAN, Enterprise SSD, TPM 2.0
    • The TCL 34R83Q is Insane
    • The TCL 34R83Q is Insane… But There’s a Catch! Honest Review About a Real Product! Read Description.
    • The one with APC Back-UPS BE850G2-GR: Charge Whatever With It, From Phone to Computer And BEYOND :D
    • Thermaltake Pure 20 fan - HUGE and quiet!
    • Ubiquiti EdgeRouter X lift off
    • VIOFO A139 Quad HD Dash Camera - 3 Channels Camera in a 2 Channels Package. Confused?
    • Why Bluetooth Headphones Switch to MONO When Using the Microphone?
    • Why the Logitech PRO Racing Wheel Made Me a Better Driver
  • Home
  • Linux World
    • Alternative to finger command? Sure: pinky :)
    • Backup/restore/reinstall tips
    • Copy one file in linux with progress bar, using pv
    • DNSKEY: verify failed due to bad signature (keyid=19297): RRSIG has expired
    • DRBD: How to replace drbd backend devices
    • Debian 11: Unable to locate package glances
    • Debian 13: missing last command
    • Debian Bullseye (11): zpool command missing
    • Debian DRBD: How to resize drbd and OCFS2 online
    • Debian DRBD: How to resize drbd volume on top of LVM
    • Debian DRBD: Tips, Tricks and Commands
    • Debian Heartbeat: Commands, Tips and Tricks
    • Debian Heartbeat: STONITH errors when configuring CRM cluster
    • Debian Jessie: how to enable and start OpenVPN
    • Debian Tips & Tricks
    • Debian on mac mini: Core temperature above threshold, cpu clock throttled
    • Debian: Can't locate LWP/UserAgent.pm in @INC
    • Debian: HA (cluster) NFS with DRBD and Heartbeat
    • Debian: How to install, configure and manage ZFS
    • Debian: How to split rc.local in rc.local.stop and rc.local.start
    • Debian: Install and configure AFP server
    • Debian: LVM on ISCSI not activated on reboot.
    • Debian: The repository does no longer have a Release file
    • Debian: add and configure VLAN
    • Debian: how to choose default kernel in grub2
    • Debian: how to create bonding device
    • Debian: install open-vm-tools
    • Debian: pdo_mysql.so: undefined symbol: pdo_parse_params in Unknown on line 0
    • Debian: reinstall zfs when changing the linux kernel
    • Debian: ssh key no longer works after upgrading to Debian version 9
    • Docker: Pi-Hole - Failed to set capabilities on file `/usr/bin/pihole-FTL' (Operation not supported)
    • Gnome: Clear "Open With" list
    • How to add a swap file in linux
    • How to add an ISCSI disk from linux server
    • How to add exfat support on RHEL 6
    • How to be sure network interfaces have always the same names
    • How to check which local device is assigned to which iscsi disk
    • How to configure GlusterFS with a volume replicated over 2 nodes
    • How to create an encrypted image
    • How to create bond device under linux
    • How to create/modify a file's timestamp
    • How to give more memory to Lotus Notes in Linux
    • How to install bind (name server) in chroot environment
    • How to install mplayer on RHEL 6
    • How to install openoffice 4.0.1 on Fedora 20/19/18, CentOS/Red Hat (RHEL) 6.5
    • How to install python 2.7 on Red Hat Enterprise Linux Workstation release 6.5 (Santiago)
    • How to modify the hostname on Linux
    • How to mount a HFS partition in linux as Read/Write?
    • How to properly configure oidentd for IRC connections
    • How to read MBR
    • How to remove LVM snapshots if you get message "Can't remove open logical volume"
    • How to setup OpenVPN server for iPhone
    • How to show UUID of a block device, including other attributes too
    • How to speed up things by using tmpfs (your memory)
    • How to switch from pc to efi and recreate boot partition
    • How to test ethernet speed with iperf
    • How to write CD or DVD under linux's console
    • ISCIS: remove disk from linux client
    • LVM
    • Linux Network Tips and Tricks
    • Linux: CPU utilization vs. load average
    • Linux: How to clear (delete) a GUID partition table (GPT)
    • Linux: TCP keepalive or not
    • Linux: Tuning network cards on Linux
    • Linux: how to check if the disk is SSD or not
    • Network Manager - get_secrets_cb(): Couldn't get connection secrets
    • RHEL6: yum update - package is a duplicate with package
    • RPM Tips and Tricks
    • ReiserFS: how to recover deleted files
    • ShellShock - Fix Bash on Debian Lenny
    • ShellShock - Fix Bash on Debian Squeeze
    • Spamassassin: eval/lookup failed: available_nameservers: No DNS servers available!
    • Start restricted graphical apps
    • Systemd startup order
    • The Nano Incident: How a Text Editor Almost Took Down My Database
    • Ubuntu: Fast Install With VMware Fusion
    • User Management
    • Using Mdadm
    • Wi-FI KO after upgrade to RHEL 6.5
    • ZFS: Convert a One Disk Pool in Two Disks Mirror
    • ZFS: cannot import 'whatever': no such pool available
    • how to resize iscsi volume in Linux
    • rc.openvpn for slackware
  • Proxmox
    • Proxmox LXC Recovery: What Happens When Your LVM-Thin Pool Runs Out of Space
  • Solaris World
    • Tips & Tricks
  • Windows World
    • Active Directory Command Line (CMD) Tips & Tricks
    • Excel: extract text between two different characters
    • Excel: seconds in hours:minutes:seconds
    • PowerShell tips and tricks
    • Windows 7: How to repair boot partition
    • Windows 7: Steam not starting
    • Windows 7: how to disable/enable hibernation
    • Windows CMD commands
  • Tips & Tricks
    • Apache Tips & Tricks
    • Audio / Video *nix Command Line Tricks
    • Bombardier: How to use it to Download Boot Camp Drivers
    • Brigadier: How to use to Download Boot Camp Drivers
    • Citrix XEN: Install VM OS via CLI
    • Citrix Xen CLI Tips & Tricks
    • Citrix Xen: Connect to VM's VNC console
    • Citrix Xen: How to "migrate" VHD lv from one XEN to another with dd
    • Citrix Xen: How to attach an existing vdi to any virtual machine
    • Citrix Xen: How to rename a vm via command line
    • Citrix Xen: How to show vm information via command line
    • Citrix Xen: Modify VM memory value via CLI
    • Docker Tips and Tricks
    • Docker: Create a Custom Image
    • Docker: How to Check Which Docker Container is Using the Space in Linux
    • Docker: How to Run Your Own Docker Registry (e.g. Images Repository)
    • Docker: Install on Debian
    • Docker: Playing with consul
    • Docker: UNIX socket and TCP port
    • Docker: how to enable swap limit capabilities
    • Docker: how to increase php memory "the docker way"
    • Docker: syslog-ng setup
    • Drupal 7 to Drupal 10 Migration and Configuration Notes
    • Executing commands remotely on multiple systems via SSH
    • FIO Perf Tool Nutshell
    • Flushing the ARP cache
    • FreeNAS console tricks
    • Gallery2 - Gd - How to create thumbnails with fixed width while keeping the aspect ratio (variable height)
    • Gallery2 - Imagemagick - How to create thumbnails with fixed width while keeping the aspect ratio (variable height)
    • Gallery2 - Netpbm - How to create thumbnails with fixed width while keeping the aspect ratio (variable height)
    • How to change the hostid of a Linux operating system
    • How to create a hash password from text
    • How to delete files or directories that contains special characters
    • How to display the output of a file without the blank lines
    • How to enable rstatd
    • How to encrypt a file in linux (or cygwin)
    • How to find out which process is using some port
    • How to login to youtube account via Apple TV if you have Google Authenticator enabled
    • How to resume failed scp transfer
    • How to rotate pdf files with pdftk tool
    • How to send a json from command line
    • How to use an LVM partition as VirtualBox disk
    • IBM DS System Storage common commands examples
    • ImageMagick Tricks
    • Logging ssh sessions
    • Merge mp3 files
    • Miscellaneous
    • MySQL Commands, Tips and Tricks
    • Nagios Stuff
    • Nagios: UNKNOWN: Drive S/N : No health status line found,
    • Networking - questions and answers
    • Oracle: How to connect to the listener and display a parameter's value
    • Oracle: How to list DBA users for a database
    • Oracle: dbfsize Header block magic number is bad
    • Oracle: display tablespace information with sizes
    • PHP: How to display in a table, information from a text file
    • Python: fake smtp server
    • Render Blocking JavaScript and CSS
    • Runlevels
    • SAP useful commands for UNIX administrator
    • SMF Tips and Tricks
    • Script to get memory usage of desired processes
    • Strings or File Manipulation
    • The Hard Link
    • Using Find Command
    • Using VI Editor
    • VirtualBox Tips&Tricks
    • VirtualBox: How to Add Serial Port for Linux Cluster Heartbeat Monitoring
    • VirtualBox: How to work with ISCSI disks
    • dsmc commands
    • iMessage “Waiting for Activation” Error on iPhone
  • Exclamation!
    • Bash CVE-2014-6278 CVE-2014-6277
    • Heartbleed Bug
    • PHP2Shell: Critical PHP Remote Code Execution Vulnerabilities
    • Ransomware KeRanger attached to Transmission app
    • Shellshock (CVE-2014-6271)
  • About This
    • Cookies, Cookies, Cookies!
  • Donate
User account menu
  • Log in
Popular content

Today's:

  • PHP2Shell: Critical PHP Remote Code Execution Vulnerabilities and Website’s Impact
  • €6,000 MacBook Pro: Should I Throw It Away After 6 Years?
  • Drupal 7 to Drupal 10 Migration and Configuration Notes
  • The Nano Incident: How a Text Editor Almost Took Down My Database
  • Proxmox LXC Recovery: What Happens When Your LVM-Thin Pool Runs Out of Space
  • Assetto Corsa Rally vs Real Life – La Bollène-VĂ©subie → Col de Turini | Monte-Carlo Rally
  • Linux Networking: How to disable/enable offload features, RX/TX checksum, scatter, gather and beyond
  • How I Test Mousepads: Glide, Control, and Surface Noise Explained
  • Windows World
  • Hardware

All time:

  • Linux Networking: How to disable/enable offload features, RX/TX checksum, scatter, gather and beyond
  • Tivoli Storage Manager Client Commands (dsmc commands)
  • Centrify Useful Commands and Information
  • Run ZIM on OS X and create Zim.app
  • How to be sure network interfaces have always the same names
  • Linux backup/restore/reinstall
  • How to add exfat support on RHEL 6
  • Mafia II Joe's Adventures - Playboy Locations
  • HMC commands
  • Command Line Software Update in MacOS

Last viewed:

  • PHP2Shell: Critical PHP Remote Code Execution Vulnerabilities and Website’s Impact
  • Macports: Current platform "darwin 17" does not match expected platform "darwin 16"
  • AIX System Dump Commands
  • Drupal 7 to Drupal 10 Migration and Configuration Notes
  • VIOFO A139 Quad HD Dash Camera - 3 Channels Camera in a 2 Channels Package. Confused?
  • The Nano Incident: How a Text Editor Almost Took Down My Database
  • Proxmox LXC Recovery: What Happens When Your LVM-Thin Pool Runs Out of Space
  • Macports: Current platform "darwin 14" does not match expected platform "darwin 13"
  • How to speed up things by using tmpfs
  • Errpt - Logopen: open: No such file or directory

Breadcrumb

  1. Home

Drupal 7 to Drupal 10 Migration and Configuration Notes

Profile picture for user root
By root, 6 November, 2026

Introduction

This is the story of how I migrated this website from Drupal 7 to Drupal 10. It was not a straightforward upgrade, but rather a gradual reconstruction of the website, with a fair amount of investigation, troubleshooting and occasional surprises along the way.

The migration started after a security incident affecting the original hosting environment. Before moving forward, I had to investigate what happened and recover the original installation. That part of the story is documented separately:

Security incident and recovery of the original website

Once the immediate problems were addressed, I decided it was time to move the website to a maintained version of Drupal. I wanted to preserve as much of its content and history as possible, while rebuilding the application using compatible modules and a modern Drupal installation.

There was no intention of starting from scratch. The articles, their original publication dates, historical statistics and existing structure were all part of what needed to survive the migration.

1. Setting up the new environment

I started with a clean Drupal 10 installation, using the following environment:

  • Drupal 10.6.20
  • PHP 8.5.12
  • MariaDB
  • Composer-managed Drupal core and contributed modules
  • Olivero as the base theme, with a custom subtheme

For the migration, I imported the old Drupal 7 database temporarily into the new MariaDB server. Having both databases available at the same time made it considerably easier to investigate differences, compare records and recover historical information.

I used the following database names:

Old Drupal 7: old_tar_gz_ro
New Drupal 10: new_tar_gz_ro

One particularly useful decision was to preserve the original content NIDs (node IDs). This allowed me to compare old and new records directly, without having to establish a separate mapping between the two databases.

It also made subsequent investigations much easier.

2. Bringing the old BBCode content back to life

2.1. The first visible problem

After migrating the articles, I noticed that some of the content was not displaying as it should.

The problem was not necessarily missing content. The original website had used BBCode extensively, and the migrated article bodies still contained all those familiar tags. Drupal 10, however, had no reason to understand them.

Years of articles depended on that formatting, including headings, lists, links, images, embedded videos and code examples. Rewriting all that content would have been an unreasonable amount of work.

I therefore decided to preserve the original BBCode and find a way to make the new installation understand it.

2.2. Introducing XBBCode

The solution was the XBBCode module, which I installed through Composer:

composer require drupal/xbbcode

I configured the migrated content to use the following text format:

xbbcode

XBBCode supports custom tag definitions through its administration interface, which meant I could recreate the formatting rules used by the old website without modifying the original article bodies.

2.3. Recreating the old tags

The original website used quite a few BBCode tags, so I had to recreate them in the new installation.

In total, I created 33 tag definitions covering formatting, headings, alignment, lists, links, images, YouTube videos, quotes, code blocks, tables and other functionality.

The definitions include:

  • Formatting: b, i, u, s
  • Headings: h1, h2, h3, h4, h5, h6
  • Alignment: center, left, justify
  • Lists: list, ol, ul, li
  • Links and media: url, img, youtube, wikipedia
  • Other: quote, code, color, size, font, abbr, acronym, table, hr, sub, sup, node, php, define

I created three tags manually first, then imported the remaining definitions in bulk using Drupal's entity storage API.

The tag templates use Twig variables such as:

{{ tag.content }}
{{ tag.option }}
{{ tag.source }}

One detail worth mentioning is the PHP tag. It displays the supplied source as code, just as it did on the old website. It does not execute PHP.

2.4. An unexpected problem with lists

Most of the tags were relatively straightforward to recreate, but the original list syntax presented a small complication.

The old website used asterisks to identify individual list items:

[list]
[li]First item
[/li][li]Second item
[/li][/list]

Unfortunately, XBBCode's parser does not accept an asterisk as a tag name. This meant I couldn't simply register the old list item marker as another custom tag.

Rather than modifying XBBCode itself, I created a small custom module named legacy_bbcode. Its job is to convert the old list syntax into standard li tags before XBBCode processes the content.

The module consists of:

modules/custom/legacy_bbcode/
├── legacy_bbcode.info.yml
└── src/Plugin/Filter/LegacyBbcodeListFilter.php

The filter runs before XBBCode in the text format processing order.

Importantly, this conversion does not change the content stored in the database. It happens during text processing, when Drupal prepares the article for display.

Status: Working and tested.

2.5. Adjusting quote formatting

Another small detail was the appearance of quoted text.

The initial template was simply:

<blockquote>{{ tag.content }}</blockquote>

However, the default appearance was too large for the migrated articles. I wanted something closer to the original presentation, without having to introduce another theme-specific CSS modification.

I therefore adjusted the template directly in the XBBCode tag definition:

<blockquote style="margin: 0.6em 0; padding: 0.4em 1em; border-left: 3px solid #888; font-size: 0.9em; line-height: 1.4;">
{{ tag.content }}
</blockquote>

This keeps the formatting self-contained within the tag definition.

2.6. Code blocks and multiline content

Code blocks required a little more investigation.

The current template is:

<pre><code>{{ tag.source }}</code></pre>

I noticed that a newline immediately after the opening code tag can produce an empty first line in the rendered block.

Looking into the XBBCode source, particularly TagElement.php and TemplateTagPlugin.php, showed that the original source is passed to the template without automatically trimming it.

I haven't made a final decision about changing this behaviour yet. Preserving intentional trailing blank lines is also something worth considering before introducing any trimming.

For now, the existing implementation works, and this remains a minor formatting detail to revisit.

3. Recovering historical statistics and view counts

3.1. Bringing back the old Statistics functionality

Preserving the articles was only part of the job. I also wanted to retain their historical view counts.

The old Drupal 7 website used the core Statistics module, together with its Popular content block. For Drupal 10, I installed the contributed replacement:

composer require drupal/statistics

The installed version is 1.0.1.

It provides a Popular content block with lists such as All time, Today's and Last viewed. Once installed, the block can be placed through the usual Block layout administration interface.

3.2. Recovering the historical counters

The old database contained a node_counter table with the historical view counts. Since the original NIDs were preserved, transferring those records was relatively straightforward.

The relevant columns are:

nid
totalcount
daycount
timestamp

I used the following query to transfer the counters:

INSERT INTO new_tar_gz_ro.node_counter
    (nid, totalcount, daycount, timestamp)
SELECT
    nid, totalcount, daycount, timestamp
FROM old_tar_gz_ro.node_counter
ON DUPLICATE KEY UPDATE
    totalcount = new_tar_gz_ro.node_counter.totalcount + VALUES(totalcount);

There is one important detail here: this operation adds the old total counts to existing new counts. It is not an operation that can be safely repeated without considering what is already in the destination table.

I kept that in mind during the subsequent testing.

3.3. Adding view counts to article teasers

With the historical counters available, I wanted to display the number of views alongside the author and publication date in article teasers.

This also needed to work for promoted content displayed on the front page.

Instead of modifying Drupal or Olivero directly, I created another small custom module, node_views.

Its definition is:

modules/custom/node_views/node_views.info.yml

Contents:

name: Node Views
type: module
description: Displays content view counts in node teasers.
core_version_requirement: ^10 || ^11
package: Custom

The node_views.module file contains:

<?php

/**
 * Implements hook_preprocess_node().
 */
function node_views_preprocess_node(&$variables) {
  if ($variables['view_mode'] !== 'teaser') {
    return;
  }

  $node = $variables['node'];

  $count = \Drupal::database()
    ->select('node_counter', 'nc')
    ->fields('nc', ['totalcount'])
    ->condition('nid', $node->id())
    ->execute()
    ->fetchField();

  $variables['view_count'] = (int) $count;
}

The module reads the total count from the Statistics table and makes it available to the node template through the view_count variable.

3.4. Making the count visible in Olivero

Getting the count into the template was only half the solution.

Olivero uses a separate template for teaser rendering:

node--teaser.html.twig

The template embeds the Olivero teaser component using an only context. This means additional variables are not automatically available inside the component.

I therefore had to explicitly pass the new variable:

view_count: view_count,

I then modified the metadata line to include the count:

{% if view_count is defined %}, views: {{ view_count }}{% endif %}

The result looks like this:

By root, 20 May, 2026, views: 1231

Status: Working.

A cache clear was necessary before the changes appeared.

3.5. Discovering orphaned statistics records

While testing the Popular content block, I encountered an Undefined array key warning in StatisticsPopularBlock.php, inside nodeTitleList().

After investigating the problem, I found that the Statistics module was trying to load nodes referenced by node_counter that did not exist in the new node_field_data table.

In other words, some historical statistics records had no corresponding content.

I used the following query to identify them:

SELECT
    nc.nid,
    nc.totalcount,
    nc.daycount,
    FROM_UNIXTIME(nc.timestamp) AS last_viewed
FROM new_tar_gz_ro.node_counter nc
LEFT JOIN new_tar_gz_ro.node_field_data n
    ON n.nid = nc.nid
WHERE n.nid IS NULL
ORDER BY nc.totalcount DESC;

The result was 62 orphaned records.

I wanted to establish whether these were articles that had been lost during migration or records that were already orphaned in the original database. To find out, I compared them against the old Drupal 7 node table:

SELECT
    nc.nid,
    nc.totalcount,
    o.title AS old_title,
    o.type AS old_type
FROM new_tar_gz_ro.node_counter nc
LEFT JOIN old_tar_gz_ro.node o
    ON o.nid = nc.nid
LEFT JOIN new_tar_gz_ro.node_field_data n
    ON n.nid = nc.nid
WHERE n.nid IS NULL
ORDER BY nc.totalcount DESC;

All 62 records had NULL values for both old_title and old_type.

That answered the question: none of these NIDs existed in either the old or new content tables. They were already orphaned records in the original statistics data, and I had simply copied them along with the rest of the table.

Before considering their removal, I prepared a backup:

CREATE TABLE new_tar_gz_ro.node_counter_orphans_backup
LIKE new_tar_gz_ro.node_counter;

INSERT INTO new_tar_gz_ro.node_counter_orphans_backup
SELECT nc.*
FROM new_tar_gz_ro.node_counter nc
LEFT JOIN new_tar_gz_ro.node_field_data n
    ON n.nid = nc.nid
WHERE n.nid IS NULL;

The deletion command is:

USE new_tar_gz_ro;

DELETE nc
FROM node_counter nc
LEFT JOIN node_field_data n ON n.nid = nc.nid
WHERE n.nid IS NULL;

My initial attempt failed because I had not selected a database. Selecting new_tar_gz_ro first resolved the error.

After deletion, the following query can be used to verify the result:

SELECT COUNT(*) AS orphaned_records
FROM node_counter nc
LEFT JOIN node_field_data n ON n.nid = nc.nid
WHERE n.nid IS NULL;

The expected result is 0.

Status: Orphan records identified and backed up. Deletion and final verification still require confirmation.

4. Giving the new website its own Olivero theme

Initially, I used the standard Olivero theme that comes with Drupal 10.

However, I knew that I would eventually need to make changes to templates and styling. Modifying the original theme directly would not be a good idea, as those changes could be lost during future updates.

I therefore created a custom subtheme:

themes/custom/olivero_custom/

Its theme definition is:

name: Olivero Custom
type: theme
description: Custom Olivero theme overrides.
core_version_requirement: ^10 || ^11
base theme: olivero

I also explicitly declared the Olivero regions:

regions:
  header: Header
  primary_menu: Primary menu
  secondary_menu: Secondary menu
  hero: Hero (full width)
  highlighted: Highlighted
  breadcrumb: Breadcrumb
  social: Social Bar
  content_above: Content Above
  content: Content
  sidebar: Sidebar
  content_below: Content Below
  footer_top: Footer Top
  footer_bottom: Footer Bottom

The idea is simple: Olivero continues to provide the inherited assets and functionality, while my own changes live in a separate theme.

4.1. Keeping template modifications separate

The templates that need modification are copied into the custom theme rather than edited in Drupal core.

For example:

themes/custom/olivero_custom/templates/content/node.html.twig
themes/custom/olivero_custom/templates/content/node--teaser.html.twig

This way, future Drupal core updates should not overwrite my custom modifications.

As usual with Twig changes, Drupal caches need to be cleared before the updated templates are discovered and compiled.

4.2. Theme settings and block placement

One thing I had to keep in mind when switching from Olivero to Olivero Custom was that theme settings and block placements are associated with the active theme.

I therefore cannot simply assume that everything configured under the original Olivero theme will automatically be available in the new one.

The block layout and theme configuration need to be checked separately.

Status: Custom subtheme created. Final migration of all theme-specific settings and placements still needs to be checked.

5. Rebuilding the parent and child content navigation

Another feature I wanted to preserve was the way the old website handled content hierarchy.

In Drupal 7, parent menu entries could have child article links. For example, visiting the Hardware page would provide a list of articles belonging to that section.

I wanted to reproduce this behaviour dynamically in Drupal 10, without manually maintaining a separate article list on every parent page.

5.1. Checking the migrated menu structure

Before looking for a solution, I first wanted to make sure the original menu hierarchy had actually survived the migration.

The relevant tables in the new database are:

  • menu_link_content
  • menu_link_content_data

One detail worth noting is that the menu link UUID is stored in menu_link_content, not menu_link_content_data.

I used the Hardware page as my test case. Its node ID is 403, and its menu link UUID is:

2f73edfa-1b5f-4523-bf06-c75280c57917

The parent had 27 direct child menu links in the main menu.

5.2. Trying Menu Children Views filter

I first installed the contributed Views module:

composer require 'drupal/views_menu_children_filter:^4.0'

The installed version is 4.0.2.

It provides a contextual filter called Menu Children, intended to let a View list child content based on a parent node.

The idea was to use the Content ID from the URL as the contextual filter argument and restrict the results to the relevant menu.

During testing, I also encountered a PHP 8.5 deprecation warning:

Using null as an array offset is deprecated

After investigating the issue, I traced it to the contributed module calling Drupal's queueTable() without an alias, while the core Views implementation uses that value as an array key.

This is a non-fatal PHP compatibility warning. It does not mean the menu hierarchy itself is broken.

5.3. Looking into Menu Entity Index

I also installed Menu Entity Index to see whether exposing the menu hierarchy through a database index would make the task easier.

The module created the menu_entity_index table, which stores information including:

  • Menu name
  • Hierarchy level
  • Menu link entity ID
  • Parent entity ID and UUID
  • Target entity type and ID

To check whether the Hardware children were available, I ran:

SELECT
    mei.level,
    mei.entity_id AS menu_link_id,
    mld.title AS menu_link_title,
    mld.weight,
    mei.target_type,
    mei.target_id,
    nfd.title AS content_title
FROM menu_entity_index mei
JOIN menu_link_content_data mld
    ON mld.id = mei.entity_id
LEFT JOIN node_field_data nfd
    ON nfd.nid = mei.target_id
    AND nfd.langcode = mei.target_langcode
WHERE mei.menu_name = 'main'
    AND mei.parent_uuid =
    '2f73edfa-1b5f-4523-bf06-c75280c57917'
    AND mei.target_type = 'node'
ORDER BY mld.weight ASC;

The query successfully returned all 27 entries.

So the important part was already there: the migrated menu hierarchy was intact and could be queried.

What I haven't completed yet is the final reusable solution that would turn this information into dynamic child article listings through Views or a small custom module.

Status: Menu hierarchy verified in the index. Reusable Views integration or custom dynamic listing solution still to be completed.

6. Cookie consent and Google Analytics

With the content and navigation taking shape, I also started configuring the services that the old website used.

For cookie consent, I installed EU Cookie Compliance:

composer require 'drupal/eu_cookie_compliance:^1.29'

I also installed the Google Tag module.

The intention is to prevent Google Analytics and Google Tag Manager scripts from running before the visitor has given the required consent.

The relevant Google script URLs are:

https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX
https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXXX

The actual Google Analytics measurement ID and Google Tag Manager container ID need to come from their respective Google accounts.

I used the EU Cookie Compliance JavaScript disabling configuration to prevent the relevant scripts from loading without consent.

This is one of those things where simply seeing the consent banner is not enough. The actual network requests need to be checked to make sure tracking is not happening prematurely.

Status: Configuration was being tested. Both pre-consent and post-consent behaviour still need verification.

7. A few other things along the way

Not everything during the migration was related to content, statistics or menus. There were also a few smaller details worth documenting.

7.1. Favicon

The old Drupal 7 favicon can be found in the original theme or site files.

The new favicon is configured through the active theme's appearance settings.

7.2. AdSense

The old AdSense blocks were present, but their visibility and rendering needed further investigation.

At this stage, I was testing the new website using an IP address rather than its final public hostname.

AdSense approval and hostname configuration may affect ad delivery, but that is separate from whether the Drupal block itself is rendering correctly. Both need to be checked independently.

7.3. What about the old Drupal 7 theme?

I also considered how much of the old website's appearance could be reused.

A Drupal 7 theme cannot simply be installed as a Drupal 10 theme. However, that doesn't mean everything needs to be recreated.

CSS, images, icons, fonts and some JavaScript can usually be reused, subject to compatibility changes.

The PHP template files and preprocessing logic are a different matter. Those need to be ported to Drupal 10's Twig templates and theme APIs.

7.4. Composer and PHP compatibility

Composer is now used to manage Drupal core and contributed modules, which makes keeping track of dependencies considerably more manageable.

During testing, I also encountered a PHP deprecation warning in the system's JsonSchema package involving the old $http_response_header variable.

This turned out to be unrelated to the Drupal content migration, but it is another compatibility detail worth keeping in mind when running the new environment on PHP 8.5.

8. Things worth remembering

A few lessons from the migration are worth keeping in mind as the work continues:

  • Keep custom code in modules/custom and themes/custom instead of modifying Drupal core or contributed modules.
  • After changing Twig templates, clear Drupal caches before assuming the changes haven't worked.
  • Be careful when importing historical counters. The import can add counts to existing records, so don't rerun it blindly.
  • Keep database backups before executing DELETE or UPDATE statements.
  • When investigating migrated content, comparing the old and new records using NID and content type can save a lot of time.
  • Legacy BBCode syntax is part of the content itself. Keeping it supported is preferable to rewriting years of articles.

9. What's left to do?

The migration has come a long way, but there are still a few things I want to verify before considering it complete.

  • Statistics: Confirm orphaned counter deletion and verify that the Popular content block no longer generates warnings.
  • Theme: Confirm all required template overrides are in the custom subtheme and check block placements and theme settings.
  • Menu listings: Decide on a reusable dynamic Views or custom module solution for listing child content.
  • Cookie consent: Test Google Analytics behaviour both before and after consent.
  • AdSense: Recheck after the website is running on its final public domain.

This is an ongoing migration log, not a finished technical specification. I'll update it as the remaining work is completed, keeping the actual commands and configuration that were successfully tested along the way.

  • Log in to post comments
  • 28 views

Thou shalt not steal!

If you want to use this information on your own website, please remember: by doing copy/paste entirely it is always stealing and you should be ashamed of yourself!
Have at least the decency to create your own text and comments and run the commands on your own servers and provide your output, not what I did!

Or at least link back to this website.

Popular content

Today's:

  • PHP2Shell: Critical PHP Remote Code Execution Vulnerabilities and Website’s Impact
  • €6,000 MacBook Pro: Should I Throw It Away After 6 Years?
  • Drupal 7 to Drupal 10 Migration and Configuration Notes
  • The Nano Incident: How a Text Editor Almost Took Down My Database
  • Proxmox LXC Recovery: What Happens When Your LVM-Thin Pool Runs Out of Space
  • Assetto Corsa Rally vs Real Life – La Bollène-VĂ©subie → Col de Turini | Monte-Carlo Rally
  • Linux Networking: How to disable/enable offload features, RX/TX checksum, scatter, gather and beyond
  • How I Test Mousepads: Glide, Control, and Surface Noise Explained
  • Windows World
  • Hardware

All time:

  • Linux Networking: How to disable/enable offload features, RX/TX checksum, scatter, gather and beyond
  • Tivoli Storage Manager Client Commands (dsmc commands)
  • Centrify Useful Commands and Information
  • Run ZIM on OS X and create Zim.app
  • How to be sure network interfaces have always the same names
  • Linux backup/restore/reinstall
  • How to add exfat support on RHEL 6
  • Mafia II Joe's Adventures - Playboy Locations
  • HMC commands
  • Command Line Software Update in MacOS

Last viewed:

  • PHP2Shell: Critical PHP Remote Code Execution Vulnerabilities and Website’s Impact
  • Macports: Current platform "darwin 17" does not match expected platform "darwin 16"
  • AIX System Dump Commands
  • Drupal 7 to Drupal 10 Migration and Configuration Notes
  • VIOFO A139 Quad HD Dash Camera - 3 Channels Camera in a 2 Channels Package. Confused?
  • The Nano Incident: How a Text Editor Almost Took Down My Database
  • Proxmox LXC Recovery: What Happens When Your LVM-Thin Pool Runs Out of Space
  • Macports: Current platform "darwin 14" does not match expected platform "darwin 13"
  • How to speed up things by using tmpfs
  • Errpt - Logopen: open: No such file or directory

@ gz.ro 2011-2026
Page generated in 0.0734 seconds. Served by tar.