When content URLs change during migrations, it is always a good idea to do something to handle the old URLs to prevent them from suddenly starting to throw 404s which are bad for SEO. In this article, we'll discuss how to migrate URL aliases provided by the path module (part of D8 core) and URL redirects provided by the redirect module.
The Problem
Say we have two CSV files (given to us by the client):
- Article data provided in article.csv
- Category data provided in category.csv
The project requirement is to:
- Migrate the contents of
article.csv
as article nodes. - Migrate the contents of
category.csv
as terms of a category terms. - Make the articles accessible at the path
blog/{{ category-slug }}/{{ article-slug }}
. - Make
blog/{{ slug }}.php
redirect toarticle/{{ article-slug }}
.
Here, the term slug refers to a unique URL-friendly and SEO-friendly string.
Before We Start
- If you are new to Drupal migrations, I recommend that you read about migrating basic data to Drupal first.
- To follow along, you can refer to the sample code for this tutorial: Migrate example: Paths module on GitHub.
- You will need to install drush for executing the migrations.
Migrate Node and Category Data
This part consists of two simple migrations:
- The categories_data migration creates taxonomy terms for each category.
- The article_data migration creates article nodes.
The article data migration depends on the category data migration to associate each node to a specific category like:
# Migration processes
process:
...
field_category:
plugin: 'migration_lookup'
source: 'category'
migration: 'example_category_data'
no_stub: true
...
So, if we execute this migration, we will have all categories created as category terms and 50 squeaky new nodes belonging to those categories. Here's how it should look if we run the migrations using drush:
$ drush migrate-import example_article_data,example_category_data
Processed 5 items (5 created, 0 updated, 0 failed, 0 ignored) - done with 'example_category_data'
Processed 50 items (50 created, 0 updated, 0 failed, 0 ignored) - done with 'example_article_data'
Additionally, we will be able to access a list of articles in each category at the URL blog/{{ category-slug }}
. This is because of the path
parameter we set in the category data migration. The path parameter is processed by the path module to create URL aliases during certain migrations. We can also use the path
parameter while creating nodes to generate URL aliases for those nodes. However, in this example, we will generate the URL aliases in a stand-alone migration.
Generate URL Aliases with Migrations
The next task will be to make the articles available at URLs like /blog/{{ category-slug }}/{{ article-slug }}
. We use the example_article_alias migration to generate these additional URL aliases. Important sections of the migration are discussed below.
Source
source:
plugin: 'csv'
path: 'article.csv'
...
constants:
slash: '/'
source_prefix: '/node/'
alias_prefix: '/blog/'
und: 'und'
We use the article.csv
file as our source data to iterate over articles. Also, we use source/constants
to define certain data which we want to use in the migration, but we do not have in the CSV document.
Destination
destination:
plugin: 'url_alias'
Since we want to create URL aliases, we need to use the destination plugin url_alias provided by the path module. Reading documentation or taking a quick look at the plugin source at Drupal\path\Plugin\migrate\destination\UrlAlias::fields()
, we can figure out the fields and configuration supported by this plugin.
Process
...
temp_nid:
plugin: 'migration_lookup'
source: 'slug'
migration: 'example_article_data'
...
temp_category_slug:
# First, retrieve the ID of the taxonomy term created during the "category_data" migration.
-
plugin: 'migration_lookup'
source: 'category'
migration: 'example_category_data'
# Use a custom callback to get the category name.
-
plugin: 'callback'
callable: '_migrate_example_paths_load_taxonomy_term_name'
# Prepare a url-friendly version for the category.
-
plugin: 'machine_name'
Since we need to point the URL aliases to the nodes we created during the article data migration, we use use the migration_lookup
plugin (formerly migration
) to read the ID of the relevant node created during the article data migration. We store the node id in temp_nid
. I added the prefix temp_
to the property name because we just need it temporarily for calculating another property and not for using it directly.
Similarly, we need to prepare a slug for the category to which the node belongs. We will use this slug to generate the alias
property.
source:
plugin: 'concat'
source:
- 'constants/source_prefix'
- '@temp_nid'
Next, we generate the source, which is the path to which the alias will point. We do that by simply concatenating '/nid/'
and '@temp_nid'
using the concat plugin.
alias:
plugin: 'concat'
source:
- 'constants/alias_prefix'
- '@temp_category_slug'
- 'constants/slash'
- 'slug'
And finally, we generate the entire alias
by concatenating '/article/'
, '@temp_category_slug'
, a '/'
and the article's '@slug'
. After running this migration like drush migrate-import example_article_alias
, all the nodes should be accessible at /article/{{ category-slug }}/{{ article-slug }}
.
Generate URL Redirects with Migrations
For the last requirement, we need to generate redirects, which takes us to the redirect module. So, we create another migration named example_article_redirect to generate redirects from /blog/{{ slug }}.php
to the relevant nodes. Now, let's discuss some important lines of this migration.
Source
constants:
# The source path is not supposed to start with a "/".
source_prefix: 'blog/'
source_suffix: '.php'
redirect_prefix: 'internal:/node/'
uid_admin: 1
status_code: 301
We use source/constants
to define certain data which we want to use in the migration, but we do not have in the CSV document.
Destination
destination:
plugin: 'entity:redirect'
In Drupal, every redirect rule is an entity. Hence, we use the entity plugin for the destination.
Process
redirect_source:
plugin: 'concat'
source:
- 'constants/source_prefix'
- 'slug'
- 'constants/source_suffix'
First, we determine the path to be redirected. This will be the path as in the old website, example, blog/{{ slug }}.php
without a /
in the front.
redirect_redirect:
plugin: 'concat'
source:
- 'constants/redirect_prefix'
- '@temp_nid'
Just like we did for generating aliases, we read node IDs from the article data migration and use them to generate URIs to which the user should be redirected when they visit one of the /blog/{{ slug }}.php
paths. These destination URIs should be in the form internal:/node/{{ nid }}
. The redirect module will intelligently use these URIs to determine the URL alias for those paths and redirect the user to the path /article/{{ slug }}
instead of sending them to /node/{{ nid }}
. This way, the redirects will not break even if we change the URL alias for a particular node after running the migrations.
# We want to generate 301 permanent redirects as opposed to 302 temporary redirects.
status_code: 'constants/status_code'
We also specify a status_code
and set it to 301
. This will create 301 permanent redirects as opposed to 302 temporary redirects. Having done so and having run this third migration as well, we are all set!
Migration dependencies
migration_dependencies:
required:
- 'example_article_data'
Since the migration of aliases and the migration of redirects both require access to the ID of the node which was generated during the article data migration, we need to add the above lines to define a migration_dependency
. It will ensure that the example_article_data
migration is executed before the alias and the redirect migrations. So if we run all the migrations of this example, we should see them executing in the correct order like:
$ drush mi --tag=example_article
Processed 5 items (5 created, 0 updated, 0 failed, 0 ignored) - done with 'example_category_data'
Processed 50 items (50 created, 0 updated, 0 failed, 0 ignored) - done with 'example_article_data'
Processed 50 items (50 created, 0 updated, 0 failed, 0 ignored) - done with 'example_article_alias'
Processed 50 items (50 created, 0 updated, 0 failed, 0 ignored) - done with 'example_article_redirect'
Next steps
- Go through the source code for the migrate_example_paths module discussed above.
- Learn how to migrate CSV / JSON / XML data to Drupal.