Skip to content

The content audit before a Drupal migration: keep, merge, drop

How to export a Drupal content inventory, score each node on traffic, freshness, links and legal need, decide keep, merge or drop, and shorten the migration.

8 min readBlueKiezel
  • migration
  • content
  • drupal 7
  • planning

The fastest way to shorten a Drupal migration is to migrate less. A content audit before the project starts sorts every node into keep, merge or drop, and the resulting list is smaller than the database in almost every case we have seen. It also produces the first draft of your redirect map, which is the other thing a migration cannot launch without.

Export the inventory

Start with what Drupal knows. The node table gives you the id, type, title, status and timestamps for every piece of content, and a join to url_alias gives you the public path. On Drupal 7:

drush sql-query "SELECT n.nid, n.type, n.title, n.status, FROM_UNIXTIME(n.created) AS created, FROM_UNIXTIME(n.changed) AS changed, u.alias FROM node n LEFT JOIN url_alias u ON u.source = CONCAT('node/', n.nid) ORDER BY n.type, n.nid" > nodes.tsv

On Drupal 8, 9 or 10 the tables are node_field_data and path_alias, and the alias source is stored as /node/123 with a leading slash:

drush sql-query "SELECT n.nid, n.type, n.title, n.status, FROM_UNIXTIME(n.created) AS created, FROM_UNIXTIME(n.changed) AS changed, p.alias FROM node_field_data n LEFT JOIN path_alias p ON p.path = CONCAT('/node/', n.nid) WHERE n.default_langcode = 1 ORDER BY n.type, n.nid" > nodes.tsv

Before you open the sheet, get the shape of the site with a count per content type:

drush sql-query "SELECT type, status, COUNT(*) FROM node GROUP BY type, status"

A site with twenty-two content types where four of them hold most of the nodes, and six of them hold fewer than ten nodes each, is a site with roughly ten content types to migrate. Take note of unpublished counts too. Thousands of unpublished nodes are usually drafts nobody will finish, and unpublished content is the easiest drop decision you will make.

Repeat the export for taxonomy terms, files and users, because each is a migration of its own. Files in particular: file_managed tells you what is referenced, and a listing of sites/default/files tells you what is on disk. The difference is orphaned uploads that do not need to move.

What to measure per node

Add four columns to the sheet. Each one answers a question an editor would otherwise answer by feel.

Traffic. Export the pages report from your analytics for the last twelve months and join it on the path. Pageviews are enough. You are looking for zero versus not zero, and then for the top of the distribution. Also export the Performance report from Search Console, because a page with impressions but no clicks may still be holding a ranking you want.

Last update. Already in the export as the changed timestamp. A page untouched since 2016 is not automatically dead, but it is a candidate, and the date tells the editor which ones to read first.

Inbound links. From two sources. Internal links, which Screaming Frog reports per URL after a crawl. External links, from the Links report in Search Console or a backlink tool. A page with external links pointing at it should not be dropped without a redirect, whatever its traffic.

Legal or contractual need. Privacy policies, terms, regulatory notices, accessibility statements, grant acknowledgements, press releases that a listing rule requires you to keep. This column is a yes or no, and it is filled in by whoever owns compliance, not by the content team.

A scoring approach

Scores are not the decision. They are a way of sorting a long sheet so that the humans spend their attention on the rows where it matters.

Give each node a point in each of four categories.

| Signal | Score 0 | Score 1 | | --- | --- | --- | | Traffic | No pageviews in twelve months | Any pageviews | | Freshness | Not updated in three years | Updated within three years | | Links | No inbound links, internal or external | At least one | | Legal need | No | Yes |

A node scoring 4 is a keep, and nobody needs to read it. A node scoring 0 is a drop, and nobody needs to read that either, unless an editor claims it. Everything in between, which is usually the middle third of the sheet, is where the audit meetings happen.

Two rules override the score. Anything with a legal need is kept regardless. Anything with external inbound links is either kept or merged with a redirect, never dropped to a 404.

Then apply the three decisions.

  • Keep. Migrates as it is, same URL. Most high-scoring content.
  • Merge. Two or more pages become one. Common with event archives, near-duplicate service pages and old news that is better as one summary. The merged page gets one URL, and the others redirect to it.
  • Drop. Does not migrate. If it had traffic or links, it gets a redirect to the closest relevant page. If it had neither, it can return 404 or 410, and the sitemap simply stops listing it.

Take a site with 9,000 nodes as a worked example. Around 2,000 are unpublished and go straight to drop. Of the remaining 7,000, a scoring pass marks 1,500 as clear keeps and 2,500 as zero-score drops. The editors read the 3,000 in the middle over two weeks and keep about a third. The migration moves roughly 2,500 nodes, not 9,000. Your numbers will differ, but the shape rarely does.

Involve the editors

Editors know things the sheet does not: that the page with no traffic is the one the sales team sends as a link, that the 2015 article is quoted in a procurement document, that three service pages exist because three managers each wanted one.

Split the sheet by section owner and give each owner their slice with the scores filled in and the default decision pre-populated. Ask for two things: confirm or change the decision, and add a target URL for every merge or drop that had traffic. Give a deadline of two weeks and be explicit that rows with no response keep the default.

Keep the middle band short. If a section owner gets 400 rows to review, they will review none of them. Sorting by traffic descending and cutting the list at the point where pageviews reach zero usually brings it down to something a person can do in an afternoon.

Record the decisions in the sheet, not in email threads. The sheet is a project deliverable, and the redirect map is built from it.

How a smaller content set shortens the migration

Every node that does not migrate saves work in four places.

Fewer content types. Each content type is a migration to configure, field mappings to write and test, a display to theme and a set of templates to build. A content type with six nodes that all get dropped is a content type that does not exist on Drupal 11. On the Drupal 7 side, migrate_upgrade will happily generate migrations for every type in the source. You edit the generated list down to the types that survive.

Fewer fields. Old content types accumulate fields nobody fills in. Once the nodes are gone, the fields go with them, and each field is a mapping and a process plugin you do not have to write.

Shorter runs. Migration runs are executed repeatedly during a project. A run over 2,500 nodes with their files takes minutes. A run over 9,000 with fifteen years of file attachments can take an hour. That difference is paid on every iteration.

Less QA. Someone checks migrated content. Cutting the set by two thirds cuts the checking by two thirds, and the checks that remain are on pages that matter.

On a Drupal 7 to 11 project, the audit also decides which contrib modules matter. A field type used only by a dropped content type does not need a Drupal 11 equivalent, and a module that only exists to support it does not need to be evaluated.

From audit to redirect map

The audit sheet has, for every node, a decision and a target URL where the decision is merge or drop. That is a redirect map, one column short of complete: it needs the status code. Merges and drops with a target get 301. Drops without a target get 410 if the page had any traffic or links, or nothing at all if it had none.

Export those rows as CSV with three columns, old path, new path and status code, and hand it to the migration team. Our post on keeping every URL through a Drupal 7 to 11 migration covers what happens next: importing the map with the redirect module, matching pathauto patterns so that keep rows keep their exact alias, and verifying the whole list before and after launch.

Do the audit before the migration is scoped, not during it. A migration estimate built on 9,000 nodes and twenty-two content types is a different estimate from one built on 2,500 nodes and ten types, and the difference is the two weeks the audit takes.

Where to start

Our Drupal 7 migration service starts with this audit, because everything downstream depends on its output. If you want the inventory and the count per content type without committing to a migration yet, the fixed-price five-day assessment delivers the export, the scoring sheet and a migration estimate based on what actually needs to move.