Creating a Shipping Method in Magento 2

There are existing extensions available for many of the shipping carriers that you may choose to utilize on your Magento 2 site, but what about a shipping method of your own? Adding a custom shipping method like this is a straightforward approach that requires only a handful of files in order to implement – five, to be exact.

Let’s create a ‘Customer Pickup’ shipping method that is used to provide customers who are local to our warehouse the option of picking up their order as opposed to paying for shipping.

Our new shipping method will get its own extension, and as with other custom extensions, it will reside within a vendor folder in our site’s appcode folder. We will be using the following path for our new extension:appcodeClassyLlamaCustomerPickup For the remainder of this article, all file and path references will be in relation to the CustomerPickup folder.

We start by creating the two files that are required for all custom extensions: registration.php and module.xml. I will assume you are familiar enough with Magento 2 development to know what these two files are and why they are required, so I won’t go into depth about that here. If you are new to creating extensions in Magento 2, the DevDocs’ PHP Developer Guide is a must-read.

Configuration files

First, we have to tell Magento about our new shipping method’s existence; this is accomplished with the system.xml and config.xml files. The system.xml file will define the configuration settings available for our shipping method in the Magento 2 admin (in STORES > Configuration > SALES > Shipping Methods) and the config.xml file will set default values for those settings.

There are certain configuration settings for shipping carriers that are expected by Magento and thus should be included in all shipping methods:

  • active – This boolean value indicates whether this shipping method is active or not.
  • model – The path to the shipping method’s model.
  • title – The name of the shipping “carrier” that will be displayed in the frontend.
  • sallowspecific – This boolean value indicates whether this shipping method should be available for all countries or only those specified.
  • sort_order – This parameter tells Magento wherein the list of available shipping methods this method will display.

There are some additional settings we are including as well:

  • price – This shipping method will have a price of $0.00.
  • name – The name of this shipping method to be displayed to the customer.
  • showmethod – Whether or not to show this shipping method even when not applicable.

Our extension’s system.xml file has a default structure that should be followed:

  • Configuration elements must be created using the following node structure:
  • The <section> node must have an id value of "carriers".
  • The <group> node must have an id value equal to our new shipping code, in this case customerpickup.

By building the XML file in this manner, Magento knows where to place the configuration settings in the backend (in this case with the other shipping methods) and how to retrieve them when called.


<?xml version="1.0"?>
 * @category    ClassyLlama
 * @copyright   Copyright (c) 2017 Classy Llama Studios, LLC
<config xmlns_xsi="" xsi_noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
        <section id="carriers" translate="label" type="text" sortOrder="320" showInDefault="1" showInWebsite="1" showInStore="1">
            <group id="customerpickup" translate="label" type="text" sortOrder="0" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Customer Pickup</label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0">
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                <field id="name" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Method Name</label>
                <field id="price" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0">
                    <validate>validate-number validate-zero-or-greater</validate>
                <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Sort Order</label>
                <field id="showmethod" translate="label" type="select" sortOrder="92" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Show Method if Not Applicable</label>

Our extension’s config.xml (the file that defines the default values for the settings defined in system.xml) also has layout rules that must be followed:

  • Configuration elements must be created under the main node of <default> with a child node of <carriers> in a child node named with the shipping method code we’ve created, in this case <customerpickup>.


<?xml version="1.0"?>
 * @category    ClassyLlama
 * @copyright   Copyright (c) 2017 Classy Llama Studios, LLC
<config xmlns_xsi="" xsi_noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
                <name>Customer Pickup</name>
                <title>Free Shipping</title>

The Shipping Method Class

At this point, we’ve created the extension, but it doesn’t yet do anything for us. To get the extension working for us, we need to create the model at appcodeClassyLlamaCustomerPickupModelCarrierCustomerPickup.php.

There are some specific items worth noting when creating our custom shipping method’s class:

  • Every shipping method’s class in Magento 2 must extend MagentoShippingModelCarrierAbstractCarrier and implement MagentoShippingModelCarrierCarrierInterface.
  • Every shipping method’s class must include a minimum of two methods: getAllowedMethods and collectRates.
    • The method getAllowedMethods:
      • Must return an array of the available shipping options for our shipping method (e.g. ‘Standard Delivery”, ‘Expedited Delivery’).
      • Can include as few or as many shipping options as are applicable to the method. In our example case, there is only the one – ‘Customer Pickup’.
    • The method collectRates returns either:
      • A boolean false, which effectively disables the shipping method, excluding it from a list of shipping options.
      • An instance of MagentoShippingModelRateResult instantiated via a Magento factory.
        • If returning an instance of Result, it can have as many, or as few, methods appended to it as is applicable; each appended method is a factory-generated instance of MagentoQuoteModelQuoteAddressRateResultMethod. This model represents a shipping method and has its details set within it (e.g. Carrier code and title, method code and title, Price). In our example below, we are only attaching a single method, but the process can be repeated as many times as necessary.
  • The $_code variable in the class must be set to the unique shipping code chosen for our custom shipping method, in this case customerpickup.
  • The method $this->getConfigData is used to retrieve the configuration settings that are defined in the extension’s system.xml file (e.g. $this->getConfigData(‘name’) will return the value set for the method’s name).


 * @category    ClassyLlama
 * @copyright   Copyright (c) 2017 Classy Llama Studios, LLC

namespace ClassyLlamaCustomerPickupModelCarrier;

use MagentoQuoteModelQuoteAddressRateRequest;
use MagentoShippingModelRateResult;

class CustomerPickup
    extends MagentoShippingModelCarrierAbstractCarrier
    implements MagentoShippingModelCarrierCarrierInterface
     * Constant defining shipping code for method
    const SHIPPING_CODE = 'customerpickup';

     * @var string
    protected $_code = self::SHIPPING_CODE;

     * @var MagentoShippingModelRateResultFactory
    protected $rateResultFactory;

     * @var MagentoQuoteModelQuoteAddressRateResultMethodFactory
    protected $rateMethodFactory;

     * @param MagentoFrameworkAppConfigScopeConfigInterface $scopeConfig
     * @param MagentoQuoteModelQuoteAddressRateResultErrorFactory $rateErrorFactory
     * @param PsrLogLoggerInterface $logger
     * @param MagentoShippingModelRateResultFactory $rateResultFactory
     * @param MagentoQuoteModelQuoteAddressRateResultMethodFactory $rateMethodFactory
     * @param array $data
    public function __construct(
        MagentoFrameworkAppConfigScopeConfigInterface $scopeConfig,
        MagentoQuoteModelQuoteAddressRateResultErrorFactory $rateErrorFactory,
        PsrLogLoggerInterface $logger,
        MagentoShippingModelRateResultFactory $rateResultFactory,
        MagentoQuoteModelQuoteAddressRateResultMethodFactory $rateMethodFactory,
        array $data = []
    ) {
        $this->rateResultFactory = $rateResultFactory;
        $this->rateMethodFactory = $rateMethodFactory;
        parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);

     * @return array
    public function getAllowedMethods()
        return [self::SHIPPING_CODE => $this->getConfigData('name')];

     * @param RateRequest $request
     * @return bool|Result
    public function collectRates(RateRequest $request)
        if (!$this->getActiveFlag()) {
            // This shipping method is disabled in the configuration
            return false;
        // Check whether order is eligible for customer pickup
            // Add logic here to determine if this order is eligible for this shipping method
            // e.g. Is the entire order in stock? Does the customer have a local address?
        // If the order is not eligible, return false
        // Otherwise, continue to build and return $result which includes our shipping method's details

        /** @var MagentoShippingModelRateResult $result */
        $result = $this->rateResultFactory->create();

        /** @var MagentoQuoteModelQuoteAddressRateResultMethod $method */
        $method = $this->rateMethodFactory->create();

        // Get the title from the configuration, as defined in system.xml

        // Get the title from the configuration, as defined in system.xml

        // Get the price from the configuration, as defined in system.xml
        $amount = $this->getConfigData('price');



        return $result;

Our new shipping method can be customized however we need it to be. For example, we could check whether any items in the customer’s order are being drop shipped, and if so, not offer ‘Customer Pickup’ as an available shipping method; we could also disable our shipping method for orders placed from outside of specific regions.

Configure and test the shipping method

After enabling our new extension and clearing the cache, we can visit our site’s admin section (e.g. and head to the Shipping Methods section at STORES > Configuration > SALES > Shipping Methods. We should now see a page similar to the one below, with our new shipping method’s default configuration settings we defined earlier in the system.xml file.

To test our new shipping method, we just have to head to our store’s website like a customer would, create a qualifying order (don’t forget what rules you had previously put in place in the model), and head to the shopping cart. As you can see in the screenshot below, our new shipping method is available for us to choose.

If we proceed to checkout, we see in the screenshot below that we again have the option of Customer Pickup as our shipping method.


We have now completed the creation, configuration, and testing of a custom shipping method that allows our local customers to save on the cost of shipping and pick their qualifying orders up at our warehouse at their convenience.

This tutorial can be used to tailor your online store’s shipping logic to match what is right for you and your business, rather than being tied to native rate models that may not fit every scenario. Custom shipping methods can be a powerful tool; and since the creation process is straightforward, it makes for a handy tool in your Magento tool belt!

A Look at CSS and Less in Magento 2

Magento 2 leverages the enormous power of CSS pre-processing (via Less) to make theme customization intuitive and easily maintainable through features like variables, mixins, and imports. Less compilation is a fundamental part of M2’s static content deployment and development tools, streamlining the theming process and ensuring that frontend developers can focus on their code, not compiling their final CSS.

The conventions and structures the native Less implementation use are smartly architected and designed to allow flexible and unobtrusive customizations. But along with the power comes the simple fact that there are many different ways to skin a given problem, and some techniques are better than others. If you spend a great deal of your time theming Magento sites, you’ll definitely want to have a broad understanding of the core conventions and, by extension, the best practices we can intuit from them. We’ll be covering both in this article.

I’ll presume that you have a basic familiarity with the details on theming, Less and dev tools available in Magento’s Frontend Developer Guide, as well as core Less concepts like variables and mixins.

Bird’s-eye Overview

Let’s hit the broad highlights of how Magento natively structures Less.

There are three major locations styles are defined:

  • view/{area}/web/css/source in an extension’s root directory
    • e.g., /app/code/MyCoolVendor/MyCoolApp/view/frontend/web/css/source/_module.less
    • Note that you won’t see this in the core packages, because baseline styles for these are actually contained in the Magento/blank theme, as referenced below.
  • /lib/web/css/source, and in particular, /lib/web/css/source/lib
    • e.g., /lib/web/css/source/lib/_buttons.less
    • These constitute Magento’s UI library styles. No direct style implementations are present here, but rather reusable components (mostly mixins) intended to be utilized in a theme.
  • In a theme, in two major contexts:
    • Relative to a specific extension
      • e.g., /app/design/frontend/MyCoolVendor/my-cool-theme/Magento_Catalog/web/css/source/_extend.less
    • More generic files directly in web/css
      • e.g., /app/design/frontend/MyCoolVendor/my-cool-theme/web/css/source/_theme.less

The bread and butter of the native theme implementation is in the extension-specific files.

As a final note, the following is focused primarily on the frontend structure and presumes a theme that extends Magento/blank.

Manifest Files

While the lion’s share of theme styles are contained within extension-specific files, before those are imported we have a foundation composed of broader styles imported by manifest files (importing other manifest files importing other manifest files). There’s really nothing to these but successive lists of imports.

Let’s look at the primary starting manifest file, web/css/_styles.less relative to the theme root:

@import 'source/lib/_lib.less';
@import 'source/_sources.less';
@import 'source/_components.less';

As you can see, this vanilla version from Magento/blank imports three other manifests. _lib.less will import the UI library mixins and their associated variables, _sources.less will import core files that use those library mixins to implement certain baseline theme styles, and _components.less will import styles specific to interactive components like (in fact, only) modals.

The Main Compiled Files

Zooming our perspective out further, we find the outermost, or top-level, Less files that correspond directly with the final compiled CSS. styles-m.less and styles-l.less are the main event here, though there are email- and print-related top-level files as well. Importing _styles.less is nearly the first thing done here, but there’s a lot more to unpack.

It’s easy to see the distinction between styles-m and styles-l by observing the way they’re included in layout:

<css src="css/styles-m.css"/>
<css src="css/styles-l.css" media="screen and (min-width: 768px)"/>

styles-m contains universal and small screen styles, while styles-l is restricted by a media query making it specific to large screens. Here are the condensed contents of the Magento/blank version of styles-m.less:

@import 'source/_reset.less';
@import '_styles.less';
@import (reference) 'source/_extends.less';
//@magento_import 'source/_module.less';
//@magento_import 'source/_widgets.less';
@import 'source/lib/_responsive.less';
@media-target: 'mobile';
@import 'source/_theme.less';
//@magento_import 'source/_extend.less';

styles-l.less has a few notable differences, but it looks much the same. The most important distinction looks like this:

@media-target: 'desktop';
@media-common: false;

The different values for these two Less variables are key in determining the output of the two files.

  • Universal styles that should apply at any screen size are wrapped in the CSS guard & when (@media-common = true) { and thus only output in styles-m. (The default value of @media-common is true.)
  • Breakpoint-specific styles are defined using a mixin definition like .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) {. The imported file source/lib/_responsive.less outputs the results of such mixins within typical CSS media queries, using the same two variables in its conditionals, thus allocating only the approprite styles to each final file.

The above should make it clear why it’s important not to include “naked” styles in your Less files, but rather always wrap them in a media-width mixin or the @media-common guard. Otherwise, your styles will be duplicated between both final CSS files.

The final structure of interest in the main compiled files are the references to @magento_import, which you’ll notice are actually preceded by Less comment characters. This directive is used by Magento in its pre-processing and transformed into a succession of actual Less imports. _module.less, _widgets.less and _extend.less are not expected to be present in only one location relative to the theme; many such files may exist relative to specific extensions (whether present in the original extension package or in the appropriate sub-directory in the theme). During pre-processing, this directive is exploded into multiple normal import statements – one for each found path. This constitutes an important difference for how these filemames are treated and how you, in turn, should use them.

Important Named Files

We’ve looked at the general flow of how Magento compiles its final CSS files. Before we delve into some practical guidelines, let’s see a run-down of the most common Less files you’re going to use to implement your styles.

  • _module.less: The core styles for a specific extension. Imported from multiple locations via @magento_import.
  • _extend.less: Additional theme-specific styles for a specific extension, or for the theme as a whole. Imported from multiple locations via @magento_import.
  • _extends.less: Styles to be used by extend selectors. Imported by reference from the generic web/css location.
  • _theme.less: Theme-specific variable overrides. Imported from the generic web/css location.

As we won’t really be covering it later, _extends.less is worth touching on, and it’s worth noting that it’s included here not because you’re really likely to use it frequently, but because the name similarity with _extend.less demands some attention, lest you confuse the two. _extends.less is imported with the “(reference)” directive, meaning Less will import it for use by other files but not actually output the contents. It’s intended specifically for the definition of common style blocks that are to be used with the Less :extend selector to implement them easily in various contexts throughout the theme. (As an example, the Magento/blank version implements styles for .abs-block-title, which other selectors in the theme use via :extend(.abs-block-title).) This is similar to a mixin but is a simpler construct.

Whew! That was as lengthy a bird’s-eye view as we could want. Now that we have a good idea of Magento’s Less structure, let’s take a look at the best way to leverage those components, by way of some practical use cases you might run into.

I Want to Significantly Re-factor Large Theme Areas

We’re starting with the most ambitious use case, because it also involves the most straightforward method of customization. Any Less partial can be directly overridden by a file in the appropriate path in your theme.

  • Override /app/code/MyCoolVendor/MyCoolApp/view/frontend/web/css/source/_module.less in this location in your theme:
    • MyCoolApp/web/css/source/_module.less
  • Override /lib/web/css/source/lib/_buttons.less in this location in your theme:
    • web/css/source/lib/_buttons.less
  • Override the Magento/blank file web/css/source/components/_breadcrumbs.less in the same path in your own theme.

When you override a Less partial in this way, it entirely replaces the source file from further back in the inheritance chain or from an extension or lib source. This is the most direct way of modifying a parent theme or extension, but it is also the most unsuitable for most cases, because it involves copying forward the entirety of the original file. This method should only be used if you truly intend a wholesale overhaul that retains little of the original.

I Want to Swap the Color Palette or Change Some Aesthetic Details

Maybe you’re just interested in sprucing the native theme up with some changes in font and hue. Or perhaps you’ve created an awesome theme and want to use it on multiple stores with some color palette swapping, using themes that inherit from the first. Such modifications are precisely the use case for web/source/_theme.less.

Less variables are used prevalently throughout Magento’s core themes (and should be in yours as well) for everything from font families and sizes to colors to spacing. By convention, _theme.less is intended to be used solely for overriding the values of such variables. This technique is a fairly powerful method for achieving a unique aesthetic without touching a single line of CSS, and even if you intend to go further with your theme, you’ll no doubt make heavy use of it.

Especially regarding your color palette, note that it’s worthwhile to make use of self-contained Less variables within _theme.less to keep things well organized. The following shows an example of defining color values only once and then distributing those values to other more meaningful variables:

@mytheme-color-gray: @color-gray22;
@mytheme-color-red: #a11e11;
@mytheme-color-red-light: lighen(@mytheme-color, 15%);

@primary__color: @mytheme-color-gray;
@link__color: @mytheme-color-red;
@link__hover__color: @mytheme-color-red-light;
@header__background-color: @mytheme-color-gray;
@h1__font-color: @mytheme-color-red;

This is the only context in which color names should be referenced in variable names. Use semantic, meaningful names for any new variables you create to be used elsewhere.

I Want to Make Major Customizations to Existing Styles

If the section title seems a little generic, that’s because we’re talking about the meat and potatoes of theming – the areas where you’ll be spending most of your time. This is the middle road between just changing some variables to alter the look and totally blowing away the native theme’s styles in favor of your own structure. This is still using the parent theme as a backbone but building your own customizations from there.

As noted above, you could accomplish this with direct overrides of files from the parent theme, but this is inadvisable. Doing so involves unnecessary duplication of large portions of code, and it makes it much more difficult to identify the styles that are unique to your theme at a glance. Instead, use _extend.less for add-on styles that make your theme-specific modifications.

We’ve seen that _module.less and _extend.less are imported in the same way, and the difference between them is merely one of convention. Extensions and the native theme include the former but not the latter; you can think of _extend.less as the empty space that’s left available for your theme-specific styles. As a caveat, though, once _extend.less is implemented in a theme, any descendent themes that need to modify it will need to copy it forward like any other file.

Recall that _extend.less is imported not just from a single location in your theme, but from multiple possible locations. While you might be tempted to use web/css/source/_extend.less as a catch-all for your theme’s styles, you’re encouraged to split them into the appropriate extensions based on the areas they interact with. (e.g., build on what’s established in the Magento/blank file Magento_Customer/web/css/source/_module.less in Magento_Customer/web/css/source/_extend.less in your own theme.) This makes for a better organized and more maintainable theme structure.

While on the subject of general theming, a couple more general tips: First of all, make use of the mixins that the core UI library makes available whenever appropriate. Icons are a good example. Magento natively uses an icon font, generally utilizing :before and :after pseudo-elements to inject the right characters. While it would be possible to accomplish this manually in additional areas where you want to use icons, you’re better off using the .lib-icon-font mixin as the core code does. This ensures better consistency and stability, and the native mixin implements the -webkit-font-smoothing property for proper antialiasing, something that is easy to overlook.

Finally, avoid hard-coded values for things like colors and numerical values in your styles. Define variables in _theme.less for such values, even if they’re new vars of your own invention.

I Want to Create Styles to Accompany My Extension

In the event that you’ve written an extension to add custom functionality that affects the Magento frontend, you’re going to need CSS to accompany it. For example, say you’ve created MyCoolVendor_Subcategories, which implements the ability for category pages to show a dynamic list of subcategories. By now, you probably know exactly where the associated styles go: view/frontend/web/css/source/_module.less in the extension root directory.

Before we leave the topic, however, let’s talk about Less variables in this context. Your extension styles almost certainly should be using variables, and it’s likely they’ll be unique vars that don’t exist in the native theme. In our use cases thus far, new variables can be defined in _theme.less with impunity, because those use cases were contained within the confines of a single theme, and Less’s lazy evaluation means variables can be defined as late as we please. In the case of an extension, however, we’ve arrived at a scenario where our Less should be complete without reference to a particular theme. Your new variables should be defined with default values in _module.less itself, typically at the very top. This is a gimme if you’re developing a distributable extension, but it’s easy to miss when writing custom functionality for a specific site. If you do miss it, and if you compile static content without specifying a theme, expect to see Less compilation errors about non-existent variables when the core themes like Magento/blank are processed.

I Want to Override a Mixin

Let’s say you’d like to make some tweaks to Magento’s native styles, but the styles in question are embedded within the body of a mixin. For example, you might want a border radius on every element where the .lib-button mixin is used. You could track down every selector where it’s used and implement your style on those selectors, but it would be better if you could inject it into the body of the mixin itself. (A good strategy would be to add a border radius parameter to the mixin definition, establishing a variable to represent a default value, which could then be set in _theme.less.)

This could be a legitimate case for simply copying forward the file (web/css/source/lib/_buttons.less) that defines that mixin. Note, though, that many such files group multiple related mixins together, and therefore there is some unnecessary duplication if we want to override only one. You might try implementing your mixin override in a path like web/css/source/custom/lib/_buttons.less.

With this strategy, we’ve reached a scenario where we actually have a new Less partial that Magento will not import by default. This is where the manifest files we reviewed above come into play. Since web/css/_styles.less and its subordinates are merely lists of imports, it’s not very intrusive at all to copy them forward and modify them. You could copy _styles.less itself forward and add your new file to the list there; this has the advantage of less duplication of core files if you have multiple new imports to add. Or if you want to be very exact about placing your file in the exact same place in the import order as the original, you could copy forward that particular manifest file (in this case, web/css/source/lib/_lib.less). Either is a fine approach.

As a final note, you might be tempted to think of overriding a mixin definition in this way as analogous to overriding a method in a programming language. It’s really not; mixin definitions are cumulative (something demonstrated clearly by the media breakpoint mixins). But Less also has enough magic at its fingertips to avoid outputting identical style declarations multiple times, so the result is still much the same. If you find you’re wanting to refactor the original mixin definition substantially, though, you’re probably better off creating a new mixin.

I Want to Define My Own Mixin

Speaking of creating your own mixin, that’s our last use case. Let’s say you create a flexbox-based implementation of a tabbed interface, which is a great case for encapsulation within a mixin. The best location for this is in lib, like the native mixins: web/css/source/lib/_flextabs.less. And you’ll want to make sure that, like the native mixins, yours supports default values for its parameters via named variables:

    @_tab-background-color: @flextab__tab-background-color,
    @_content-background-color: @flextab__content-background-color
    // ... many other parameters, I'm sure
) {
    // Styles go here ...

Now, wherever .lib-flextabs is used, @_tab-background-color can be passed in if desired. But the variable @flextab__tab-background-color is also available to set universally in _theme.less to apply to all uses of .lib-flextabs when that parameter is not explicitly passed.

Presuming you’re defining your custom mixin within a theme, you could just put your initial declaration of all such default variables directly in _theme.less and call it done. It’s probably better form, though, and will make your Less structure more intuitive for other devs, if you follow the core pattern and pair your mixin file with a variables file that defines default values. (e.g., web/css/source/lib/variables/_flextabs.less)

From here, it’s just a matter of actually making sure your new Less partials get imported, which follows the same procedure described above: Copy forward web/css/_styles.less and add them there, or else copy forward both web/css/source/lib/_lib.less and web/css/source/lib/_variables.less and add them in precisely the same spot that other mixins are imported.

In the event that you’re defining a new mixin as part of a custom extension, the procedure will be much the same. You’ll still define partials for your mixin definition and its variable default values, but you’ll put the import statements for them in your extension’s _module.less.


I hope that in our list of use cases we’ve hit on a few practical scenarios you face in your day-to-day Magento development. If you’ve struggled with the right Less code to write and the right place to put it, you should have a better level of confidence now that we’ve peeked under the hood. Taking the time to examine how Magento understands and processes your theme code is well worth it to attain a theme that’s more understandable, more extensible, and more maintainable in the long run.

Canonical Tags: Did You Get It Right on Your Magento Site?

A few weeks ago, a llama posed a seemingly simple question: “Does anyone know the details of canonical URLs and pagination?”. But here’s the thing about llamas – we don’t do well with non-definitive answers. Thus began the digging and twists and turns into the rabbit hole of canonical tags and Magento’s native capabilities.

Here is what we know to be true:

Canonical Tags

With a site full of content, products, and categories, it’s pretty common for the same content to be on multiple URLs. That’s where Canonical Tags (rel=”canonical”) come in.  This tag tells Google what your preferred URL is so that search results will be more likely to show that URL in SERPs. Without using canonical tags in these instances, you can suffer duplicate content issues. In addition, you are forcing Google to decide which to give SEO value to, or splitting its value.

Canonical tags are kind of a hybrid of a NOINDEX tag and a 301. You’re basically telling Google – 1) Don’t show the other pages with similar content in search results, and 2) Push ranking signals to this page. It also reduces a number of resources that Google Bots will spend crawling pages, so it’s a win-win-win.

Paginated Content

Paginated content occurs when you have a series. For example, 5 pages of products under the category “Women’s Shirts”. Using tags like the following indicates the relationship between these unique URLs:

The URL string used in pagination tells Google that you want them to treat the series as a logical sequence. This will then link their properties and SEO value while sending searchers to the first page.

How Magento Handles Category Pages

Natively you can turn on or off canonical tags for categories under the Catalog > Search Engine Optimization settings in your store’s configuration. Turning this setting on adds the rel=”canonical” link tag to all category pages.

Canonical Tag Configuration Settings Stores Magento Admin

This is helpful for telling search engines what the “main” version of your category page is. If you’re using layered navigation to filter a category, parameters are added to the URL for each filter which can create many variations of your main category URL. The canonical tag tells search engines to treat all of those variations of the category as if they are the “main” canonical version. Therefore any SEO value these variations provide is transferred to your main category URL.

The same is true for pagination parameters. From page 2 on, a parameter such as ?p=2 will be added to the main URL to indicate which page of the category you are on. The canonical tag is going to tell the search engine to only show the “main” version of the page in search results, but transfer any link value of the additional pages to the canonical URL, because it is likely not going to index any of the following pages. However, a canonical tag is designed to be used on pages that contain the same (or very similar) content. On a category product listing page, your category description and some content may be the same on each version, but the products listed on the page will be different for each page. That’s the whole point of pagination- to break up a large category into multiple pages. So, this presents a conflict in the way a canonical tag is meant to be used.

What do those facts tell us?

We can surmise from the above that with your Magento store, you can either tell Google that the first page in a series is the most important or let Google decide on its own. Those are currently your only two options.

Notice that there is an option recommended by Google that isn’t natively available with Magento: Use rel=”next” and rel=“prev” to indicate the relationship between component URLs. Without that option, how does one create that synchronicity in the series other than just hoping Google will figure it out? This leads to more questions – Is Magento’s default way of handling this all wrong? And does it really matter if they aren’t in line with Google’s recommendations? And then all of our heads exploded, because WHAT THE HECK. Further research and a lot of discussions have led us to some answers for these questions.

What are the potential ramifications of doing this incorrectly?

The damage occurs in a few ways. If you enable canonical tags, you’re losing potentially valuable keywords because you’re telling Google to ignore everything after the first page. If you don’t, then you run the risk of creating duplicate content issues and/or Google simply not recognizing the pagination at all.

In addition, we have also theorized that if you are migrating from a different platform that might have employed rel=next/prev, and then move to Magento where your URL structure will undoubtedly change, you can create some very undesirable results by canonicalizing one page (effectively asking Google to ignore the series).  You have a strong possibility of really damaging your SEO and seeing a big dip in organic traffic, because you are pushing Google to ignore content that it previously crawled, devaluing your pages.

What is the right course of action?

Option 1, Do Nothing.  This isn’t a great option. Yes, Google is pretty smart. But you’re relying on “hopefully they get what we’re trying to do” rather than explicitly stating your preference, and you’re leaving the SEO value of those pages to chance.

Option 2, Enabling a ‘View All’ page. This is great… if you have a small catalog. If ‘View All’ produces a page with 500 products your user experience just tanked due to slow site speed and you’re increasing the potential to lose that customer. This would only solve the problem in combination with the right customization: Enabling a “View All” page on your categories doesn’t automatically make that the URL that is used for the canonical link tag. We would have to customize Magento to do so.

That leaves us with Option 3: Use rel=”next” and rel=”prev”, which is what we are recommending. Hallelujah, we have an answer! Well, kind of.

We’ve had a lot of internal discussions about scenarios in which it would make sense to set a canonical tag in conjunction with rel-”next/prev”. In each scenario, it wouldn’t ever have been an issue if the URL structure was set to best practice. However, let’s say for some reason yours isn’t set to best practices, and you are not currently in the position to change that, you would want to set a canonical tag on the first page so that you don’t run into duplicate content issues, and in addition use rel=next/prev to identify pagination.

An example of this would be:

A site has a direct link to “Women’s Shirts” at, but this category can also be navigated to through a subcategory where the URL then changes to You would then want to set the canonical tag to the main category page but also include next/prev so that all pages are indexed, and also so that you do not encounter any duplicate content issues. This is one of those rabbit hole situations I referred to earlier. Because technically, having both of these categories/subcategories with the same content but a different URL path is a wrong taxonomy. Best SEO practices dictate that no matter how you navigate to a page, the URL should be the same. This example isn’t a very clean way to handle pagination and is really only a band-aid because in general, cleaning up your URL structure will solve a lot of “what if” scenarios concerning canonical or next/prev.

So, Magento didn’t technically get it “wrong”. But a broad-sweeping approach, which is what the native functionality does, won’t work for the vast majority of Magento clients. Each case should be evaluated individually to determine the best course of action.

What Next?

Identifying that using rel=next/prev rather than a canonical tag on the first page is only the first step to solving this problem. Currently, Magento does not have a native setting for using next/prev. Here at Classy Llama, we are currently working on an extension that we can use going forward* as next/prev will be the answer more times than it’s not. Having something on hand will not only save us and our clients time and money in the long run but will also reduce the risk of lost revenue when combined with other SEO practices.

Overall, technical SEO has become a big focus for Classy Llama, both in Marketing Services and on the Development teams. Keep an eye out in the coming months for a lot more information from us on the importance of this and how you can redesign or migrate your site without losing traffic and revenue.

* Update December 16th, 2017: This functionality now exists as a part of the Rebar package that is available to all Classy Llama clients.

User Acceptance Testing a Magento 2 Site

During our website builds, some of our clients have asked about our quality assurance process. We commonly get questions like “What do you check during testing?” and “What should our testing include?” We can give answers to the first question during the course of the project. In response to the second question, we recently wrote a document that provides an initial set of answers. A primary factor that influenced the direction of the document is that we’ve found that a merchant tends to focus on the features of a site that are unique to them, while overlooking aspects that are standard or only lightly customized. And having a published procedure which we can give to clients means they can get started sooner with their testing because they have a roadmap to follow. A benefit of an earlier start on user acceptance testing is more available time for changes and bug fixes prior to launching.

The first version of the procedure guides a merchant through testing a “happy path” through the site. A “happy path” is a path through a system or site where given normal, expected inputs, everything works as expected without errors or exceptions. Using a “happy path” approach doesn’t ensure that a site is bug-free since things do go wrong in the real world. Our internal testing process is much more comprehensive and detailed, and hopefully, we have delivered a nearly defect-free site to the client. Our intention with the procedure is to achieve two objectives during user acceptance testing:

  1. To prompt user acceptance testing when there’s enough time to act on the client’s feedback without delaying the launch.
  2. To ensure that the client sees what most of the site looks like, and experiences how the key features of the site work.

The Scenario

The scenario we chose to emulate with the test is that of a first-time guest user of the site. We thought about the actions that a first-time visitor would most likely take, and created a diagram depicting what that user might do.

Note that the above diagram isn’t a rigorously correct process flow diagram. It was sufficient for the purpose of guiding the writing of the test procedure. And it probably includes more actions than a typical first-time guest user would actually do. It can be thought of as a “happy path with detours”. We wanted to cover those behaviors that were highly likely to be done by first-time visitors, although any single user is unlikely to do all of them.

Test Coverage

The procedure covers the following aspects of testing the store as a first-time guest user.

  • Testing with both desktop and mobile devices.
  • Visiting the homepage.
  • Adding a product to the cart from a “featured products” or “hot sellers” block.
  • Navigating to a category page and adding a product on it to the cart.
  • Searching for a product and adding a product to the cart from the search results.
  • Adding a product to the cart from a product details page, including at least one product with options.
  • Viewing the minicart and using its features.
  • Viewing the shopping cart and using its features.
  • Checking out.
  • Reviewing the order confirmation.
  • Opening an account.
  • Using account page features.
  • Viewing the 404 page.

The Document

The document includes step by step instructions and many screenshots so that it is usable by testers who are new to Magento-based sites. It is available as a PDF file.

Classy Llama’s clients will be provided with the document at the appropriate point in their project and may also obtain it by contacting their project manager and requesting a copy of User Acceptance Testing a Magento 2 Site.

Google Analytics and Magento 2

Google Analytics is an important tool for merchants in the eCommerce and Magento communities because it allows merchants to make informed decisions about their website based on their users’ interactions. With the advent of Magento 2, it’s important to understand how the new version utilizes and integrates with Google Analytics. If you’re familiar with Magento 1, you’ll remember that Google Analytics was fairly easy to setup so you could quickly view reporting data. What about Magento 2? Don’t worry, integration with Google Analytics offers additional features with the same setup ease.

What’s the Same?

  • Universal Analytics – Magento 2 still features the same quick-and-easy Google Analytics setup. It’s just a matter of enabling the Google Analytics setting and adding your account number. Thankfully, the deprecated option of ‘Google Analytics’ (as opposed to ‘Universal Analytics’) is no longer around to add confusion during setup.

alt text

  • Adwords – this hasn’t changed in Magento 2; it’s still just as easy as adding your Conversion ID and Label.
  • E-commerce Tracking – One very powerful feature that seems to fly under the radar is eCommerce tracking. This feature is a must-have for any eCommerce site and lets you track transaction and product data on your site. If you already use Google Analytics with Magento, you get this feature with almost no setup. The only thing required to start using eCommerce tracking is to enable this feature in your Google Analytics account. Magento does the rest.

alt text

  • Google Tag Manager – Magento 2 Enterprise Edition includes the option to use Google Analytics via the flexible option of Google Tag Manager.

What’s New?

Magento 2 supports a Google Analytics feature called Content Experiments. This feature allows you to setup A/B tests on your Magento site to track how different versions of pages perform. Although its simplicity makes it a good solution for small-scale A/B tests, I don’t think it’s a good solution for complicated A/B testing.

To set up content experiments, (and after toggling the feature on in Magento admin configuration settings), there are only two steps:

1. A new experiment needs to be setup in your Google Analytics account. This is pretty straightforward. (Make sure to select the option to manually insert code and save this for the next step). You can follow the instructions from Google here.

alt text

2. Connect the new experiment with Magento. Create at least two new CMS pages, (this can be done with any number of different variants), your ‘original’ version and a ‘variant’. Place the javascript snippet from the experiment setup in the previous step under the new tab, ‘Page Experiments’. Just like other Google Analytics features, Magento handles rendering the snippet in the correct place on the front end.

alt text

That’s it! The way this simple example experiment has been set up, half of the visitors will be sent to the original page and half to the variant. You can track how your experiments are trending in the reporting section of your Google Analytics account under Behavior > Experiments.

To learn more about how Classy Llama approaches A/B testing check out this post.

What else?

A lot of our clients are interested in eCommerce tracking. And while the eCommerce tracking provided by Google Analytics provides a lot of good data, it can sometimes be hard to visualize or grasp what you’re looking at. This is where goals and funnels come in.

  • Setup your goal(s). This can be done many different ways depending on what you are wanting to track and how your user interacts with the site. Google provides documentation on setting this up different ways. A basic example of a goal is a user completing an order (reaching the success page).

alt text

  • One thing to keep in mind: By default, Google tracks views on secure urls separately from unsecured urls. So if you use a destination goal which includes both secure and unsecured pages, you will need to setup cross domain tracking.
  • The funnel visualization view (Under Conversions > Goals) can provide great insight into how users are moving through your website and reveal pages that have unusual rates of drop-off.

alt text

Another common thing to track is cross-selling/upsells. A merchant may want to know how often an advertised cross-sell product is clicked. To accomplish this, an event (or trigger if using Google Tag Manager) needs to be setup to let Google know when a cross-sell product is clicked. You can do this in Google Tag Manager, or by adding some javascript: javascript ga('send', 'event', 'cross-sell', 'click', '{product-id}'); Then in the Google Analytics dashboard, setup an event goal:

alt text

Both of these goal examples can easily be modified to include different steps in the flow or more complicated events. You can view reporting details about any of the goals you’ve set up under Conversions > Goals > Overview.

These are fairly simple examples, that just skim the top of what this feature can provide.


By expanding the features in the integration of Google Analytics, Magento 2 empowers merchants to make informed decisions about their site. And the setup ease of content experiments, eCommerce tracking, and the other Google Analytics features allow merchants to focus their efforts on improving the performance of their website.

Maintaining Excellent Customer Service at Scale for R&R Products, Inc.

Sometimes, no matter your best intentions, a project can be laden with difficulties. That was certainly true for the R&R Products, Inc., company in the early stages of their eCommerce journey. 

When the R&R team decided their new goals were to expand from the golf products industry into the landscaping market, all while keeping up their commitment to customer service, they knew they would need a stable website with a great user experience. Unfortunately, they were working with a Solutions Integrator that wasn’t able to handle the complexity of the upgrade their website needed, from M2 Open Source to M2 Commerce Cloud. 

That’s when R&R began their search for an agency partner that had the experience and confidence needed to get into the weeds of coding complexities and also the expertise to pull the project out of those same weeds. 


R&R Products, Inc., is a name that’s synonymous with quality. As the world’s leading manufacturer of turf equipment replacement parts for the commercial turf industry, their dedication to serving the needs of golf courses, athletic fields, and more with high quality products has never wavered since their founding in 1971.

Not only that, despite their catalog featuring over 32,000 quality replacement parts, accessories, and more, their team takes superior customer service so seriously, they proudly offer a 98% rate on products being shipped the same day their order is received. When you order from R&R, you can be assured that not only will you get a quality product, you’ll also get it in a timely fashion. 

Looking to the future, R&R is also pursuing growth into the general landscape industry, hopeful they can bring their brand of quality and service to another market. 

But, with a site that worked directly against these objectives, an update with better functionality wasn’t just wished for—it was an absolute necessity.


Standing in the way of the R&R team’s goals was an outdated website that wasn’t functioning at a level commensurate with their commitment to customer service. Their M2 Open Source website contained an abundance of archaic, specialty code, making it slow to load and hard to use. 

Having a stable website with a great user experience is absolutely crucial to R&R Products’ mission that every shopper gets the same excellent customer service every time they visit their website. 

Unfortunately, the complex fixes that would be needed weren’t achievable with the SI they were currently working with. Facing frustrated customers and internal team woes as well, Brian Larson from R&R felt the pressure to fix their eCommerce presentation so acutely it followed him home. He felt unable to disconnect from the stress even in his down time. If the company couldn’t get a better website launched, their eCommerce presentation would continue to stall the growth and expansion R&R saw for their brand. Not only that, their team members would suffer the continued weight of that stress. 


When the R&R team came to Classy Llama, we knew it would be a difficult project, but that didn’t dissuade us from digging in. Delivering what clients need even through storms of difficulty is what lights llamas up, so we rolled up our sleeves and got to work discovering what R&R needed to achieve their goals, and how we could make it happen for them. 

Fitment, UX, and more: Solutions for R&R

Because of the intense level of customization R&R’s M2 Open Source website started with, Classy Llama knew it wouldn’t be a simple process to move them to Adobe Commerce (Magento). Each area of their site that was built with custom code needed to be reviewed and a new—hopefully native to Magento—solution would need to be developed. From shipping to UX to their ERP, Classy Llama began the complicated process of auditing, roadmapping, and implementing each Adobe Commerce integration that would bring R&R’s website from buggy and archaic to functional and technically up-to-date. 

One feature that was intensely important to R&R’s user experience functionality and, therefore, their goals of stellar customer service, was the interactive schematics. This functionality, often referred to as, “Fitment,” is designed to make searching for parts super simple for shoppers and is extremely popular among all kinds of parts sellers. Fortunately, Classy Llama’s extensive experience in the automotive industry and dealing with large catalogs of aftermarket parts made us the team with the technical chops needed to create an excellent fitment search solution for R&R. 

After identifying that the previous SI’s work on the fitment solution would need to be scrapped, Classy Llama’s UX team got to work rebuilding the code from scratch. The team also implemented a functionality that made it possible for customers to save the models of their machinery (ex. lawn mowers) to a personal login so that when they searched, only parts for the models they own would display. 

An Ongoing Partnership

After almost two years of working through various problems during the new Adobe Commerce build, the Classy Llama and R&R teams were able to build the kind of trust and connection every brand needs in their eCommerce partner. Shortly before the official launch date, R&R signed on to an ongoing partnership with Classy Llama, to provide post launch support as well as continued maintenance on whatever they need most. 

Not only that, during our ongoing maintenance, the solutions team determined R&R would benefit from an upgrade to their Adobe Commerce site. Classy Llama is now working toward kicking off the upgrade project as soon as possible. 


Now that R&R Products, Inc., has an eCommerce partner like Classy Llama on their side, their brand is primed to achieve their growth goals. Plus, their dedication to customer service excellence has the technical back-up necessary to make sure they always keep their promise to their customers. 

Contact Us