Categories
Digital Marketing Google Ads

Google Ads Search Campaign Structure for DTC Brands

These are the 3 core Search Campaigns needed when setting up Google Ads for your DTC brand.

CampaignDescription
Branded SearchThis campaign will target your brand keywords.
Generic SearchThis campaign will target your top performing unbranded keywords
Keyword ProspectingThis will be a Dynamic Keyword campaign. The goal of this campaign is to find new profitable unbranded keywords.

Common Settings

These settings will stay the same across all of your Campaigns.

SettingValue
ObjectiveSales
Conversion GoalPurchases
Campaign TypeSearch
Campaign Goal MethodWebsite Visits
Networks: Choose NetworksInclude Search Partners
Exclude Google Display Network
LocationsSelect your primary location

Campaign:
Branded Search

This campaign targets your Branded search terms. It is mostly defensive in nature: It prevents your competitors from bidding on your brand term cost effectively. This campaign will generate the majority of your conversions, but you want to keep your cost-per-acquisition costs low, as most of the sales generated by this campaign are not incremental sales.

SettingValue
Campaign NameSearch: Brand
Bidding
What do you want to focus on?
Where do you want your ads to appear?
Percent (%) impression share to target?
Maximum CPC bid limit?

Impression Share
Top of Results Page
90%
$1.00
Daily Budget$20.00 / day
(adjust to your level of comfort)

Ad Groups

This campaign initially contains only two ad groups: One to target the exact match version of your brand name, and another to target the phrase match version.

Ad Group NameKeywordsNegatives
Brand – exactYour primary brand term, exact match

eg: [nike]
None
Brand – phraseYour primary brand term plus product names closely related to your brand, phrase match

eg: “nike”, “air jordan”
Exclude the exact match brand term, targeted in the “Brand – exact” ad group above.

eg: [nike]

Campaign:
Generic Search

This campaign will target high-performing non-branded search terms. It will initially be empty, but you will populate it as you discover high performing search terms.

SettingValue
Campaign NameSearch: Generic
Bidding
What do you want to focus on?
Set a target return on ad spend

Conversion Value
200%
(adjust to your profit margins
and comfort levels)
Daily Budget$10.00 / day
(adjust to your level of comfort)

Eventually this campaign will contain one ad group for each performing exact match search term, as well as a second ad group with the phrase match version of the performing term.

Ad Groups

Ad Group NameKeywordsNegatives
Term A – exactA high-performing generic term related to your brand, exact match

eg: [running shoes]
None
Term A – phraseA high-performing generic term related to your brand, phrase match

eg: “running shoes”
Exclude the exact match term targeted by the ad group above.

eg: [running shoes]
Term X – exactAs you discover new performing keywords, create a new exact match ad group for them.

eg: [basketball shoes]
None
Term X – phraseAlso create a phrase-match version of the ad group.

eg: “basketball shoes”
Exclude the exact match term targeted by the ad group above.

eg: [basketball shoes]

Keyword Exclusions

Your Generic Search Campaign may target your Branded search terms. Create a Negative Keyword List containing your branded terms, and add it to the campaign to ensure this does not happen.

Campaign:
Keyword Prospecting

The goal of the Keyword Prospecting Campaign is to find new high performing keywords.

When such a keyword is found it should be:

  1. Added to the Generic Search Campaign
  2. Excluded from this Keyword Prospecting Campaign
SettingValue
Campaign NameSearch: Dynamic Prospecting
Bidding
What do you want to focus on?
Set a target cost per action?

Conversions
No
Daily Budget$10.00 / day
(adjust to your level of comfort)

Ad Groups

This campaign will have a single dynamic ad group called “Catch-all”, targeting all pages on the site.

Ad Group NameAd TargetsNegatives
Catch-all
[dynamic]
All WebpagesExclude all keywords already targeted by your Generic Search Campaign.

eg:
[running shoes]
[basketball shoes]

Keyword Exclusions

This campaign will also target your Branded terms unless you explicitly exclude them. Create a Negative Keyword List containing your branded terms and add the list to the campaign.

Multi Country / Currency / Language

If you are targeting multiple countries, currencies, and languages, then you will generally want to replicate this campaign structure for each Currency x Language combination.

Categories
Digital Marketing Google Ads

Negative Keyword Lists

These are the Negative Keyword Lists that every Google Ads account should have:

List NameDescription
Brand TermsContains phrase match versions of your brand name (including misspellings) as well as names of your key products that are closely associated with your brand.

Added to most of your campaigns, to funnel all branded searches into your branded campaigns.

Example
“nike”
“air jordan”
Global Exclude: IrrelevantContains exact and/or phrase match versions of terms that are irrelevant to your product or brand.

This keyword list is added to all campaigns.

Example
“used”
“fake”
“dress shoes”
Global Exclude: Too BroadContains usually only exact match versions of very broad terms that very broadly describe your product. Although relevant, they are too high up the funnel for most brands, and very unlikely to convert. Excluding these should generally reduce your costs without affecting your revenue.

This keyword list is added to all campaigns.

Example
[shoes]
CompetitorsContains phrase match versions of your competitor’s brands. For many brands, it is relatively difficult to convert on your competitor’s branded search, and so if you want to bid on your competitor’s terms, it makes sense to have dedicated campaigns for this purpose.

Added to most of your campaigns, to funnel all competitor searches into your competitor campaigns.

Example
“adidas”
“puma”
“new balance”

Categories
Digital Marketing Google Ads

Device Bid Adjustments for Google Ads

Conversion rates can vary a great deal across devices, and so it makes sense to bid differently for Mobile vs Desktop vs Tablet. Here two methods to calculate your bid adjustment, as well as an automated bidding script that will do all the work for you!

Calculating based on Conversion Rates

The simplest way is to calculate bid adjustments based on the individual device conversion rates relative to the Campaign average

Calculating based
on Click Value

I believe a more correct way is to calculate bid adjustments based on the relative per click values of each device (the Conv. Value / Click column)

Example Using Real Data

Sample Campaign

DeviceConv. rateConv. value / clickAdjustment
(Conv. Rate)
Adjustment
(Click Value)
Mobile1.15%$6.90-40%-43%
Desktop3.01%$20.20+57%+68%
Tablet0.89%$1.85-54%-85%
Campaign1.91%$12.03

Automating Bid Adjustments

Below is a Google Ads Script that will automatically make these adjustments for you (based on relative conversion rate). You can download the latest version of this script on GitHub here:
Google Ads Device Bid Adjustment Script

// Version: 1.13.1 Muppet
// Latest Source: https://github.com/Czarto/Adwords-Scripts/blob/master/device-bid-adjustments.js
//
// This Google Ads Script will incrementally change device bid adjustments
// based on conversion rates using the Campaign's average conversion rate
// as a baseline.
//

/***********

MIT License

Copyright (c) 2016-2021 Alex Czartoryski

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

**********/

var LABEL_PROCESSING_DESKTOP = "_processing_desktop";
var LABEL_PROCESSING_MOBILE = "_processing_mobile";
var LABEL_PROCESSING_TABLET = "_processing_tablet";

var BID_INCREMENT = 0.05;       // Value by which to adjust bids
var MIN_CONVERSIONS = 10;       // Minimum conversions needed to adjust bids.
var MAX_BID_ADJUSTMENT = 1.90;  // Do not increase adjustments above this value



function main() {
    initLabels(); // Create Labels

    /*****
      Device performance *should* theoretically not vary over time
      (unless a site redesign has been performed) and so it makes
      most sense to use a relatively long time period (1 year)
      on which to base adjustments.

      Shorter time periods included for reference, but commented out
    *****/

    //setDeviceBidModifier("LAST_7_DAYS");
    //setDeviceBidModifier("LAST_14_DAYS");
    //setDeviceBidModifier("LAST_30_DAYS");
    //setDeviceBidModifier(LAST_90_DAYS(), TODAY());
    setDeviceBidModifier(LAST_YEAR(), TODAY());

    cleanup(); // Remove Labels
}


//
// Set the Processing label
// This keeps track of which bid adjustments have already been processed
// in the case where multiple time-lookback windows are being used
//
function initLabels() {
    checkLabelExists();
    cleanup();

    var itemsToLabel = [AdWordsApp.campaigns(), AdWordsApp.shoppingCampaigns()];

    for (i = 0; i < itemsToLabel.length; i++) {
        var iterator = itemsToLabel[i].get();

        while (iterator.hasNext()) {
            campaign = iterator.next();
            campaign.applyLabel(LABEL_PROCESSING_DESKTOP);
            campaign.applyLabel(LABEL_PROCESSING_MOBILE);
            campaign.applyLabel(LABEL_PROCESSING_TABLET);
        }
    }
}



//
// Create the processing label if it does not exist
//
function checkLabelExists() {

    var labels = [LABEL_PROCESSING_DESKTOP, LABEL_PROCESSING_MOBILE, LABEL_PROCESSING_TABLET];

    for (i = 0; i < labels.length; i++) {
        var labelIterator = AdWordsApp.labels().withCondition("Name = '" + labels[i] + "'").get();
        if (!labelIterator.hasNext()) {
            AdWordsApp.createLabel(labels[i], "AdWords Scripts label used to process device bid adjustments");
        }
    }
}


//
// Remove Processing label
//
function cleanup() {
    var cleanupList = [AdWordsApp.campaigns(), AdWordsApp.shoppingCampaigns()];

    for (i = 0; i < cleanupList.length; i++) {
        var iterator = cleanupList[i].get();

        while (iterator.hasNext()) {
            campaign = iterator.next();
            campaign.removeLabel(LABEL_PROCESSING_DESKTOP);
            campaign.removeLabel(LABEL_PROCESSING_MOBILE);
            campaign.removeLabel(LABEL_PROCESSING_TABLET);
        }
    }
}


//
// Set Device Bids
//
function setDeviceBidModifier(dateRange, dateRangeEnd) {

    var STANDARD = 0;
    var SHOPPING = 1;

    for (i = 0; i < 2; i++) {
        //Logger.log('---  ' + (i==STANDARD ? 'Standard Campaigns' : 'Shopping Campaigns'));

        var labels = [LABEL_PROCESSING_DESKTOP, LABEL_PROCESSING_MOBILE, LABEL_PROCESSING_TABLET];

        for (l = 0; l < labels.length; l++) {
            //Logger.log('     ' + labels[l]);

            var campaigns = (i==STANDARD ? AdWordsApp.campaigns() : AdWordsApp.shoppingCampaigns());
            var campaignIterator = campaigns.forDateRange(dateRange, dateRangeEnd)
                .withCondition("Status = ENABLED")
                .withCondition("Conversions >= " + MIN_CONVERSIONS)
                .withCondition("LabelNames CONTAINS_ANY ['" + labels[l] + "']")
                .get();

            while (campaignIterator.hasNext()) {
                var campaign = campaignIterator.next();
                var baseConversionRate = campaign.getStatsFor(dateRange, dateRangeEnd).getConversionRate();
                var platforms = [campaign.targeting().platforms().desktop(),
                    campaign.targeting().platforms().mobile(),
                    campaign.targeting().platforms().tablet()
                ];

                //Logger.log('    CAMPAIGN: ' + campaign.getName());

                var targetIterator = platforms[l].get();
                if (targetIterator.hasNext()) {
                    var target = targetIterator.next();
                    var stats = target.getStatsFor(dateRange, dateRangeEnd);
                    var conversions = stats.getConversions();
                    var conversionRate = stats.getConversionRate();
                    var targetModifier = (conversionRate / baseConversionRate);
                    var currentModifier = target.getBidModifier();

                    //Logger.log('    Conversions: ' + conversions);
                    
                    if (conversions >= MIN_CONVERSIONS) {
                        if (Math.abs(currentModifier - targetModifier) >= BID_INCREMENT) {
                            if (targetModifier > currentModifier) {
                                target.setBidModifier(Math.min(currentModifier + BID_INCREMENT, MAX_BID_ADJUSTMENT));
                            } else {
                                target.setBidModifier(Math.max(currentModifier - BID_INCREMENT, 0.1));
                            }
                        }

                        campaign.removeLabel(labels[l]);
                        //Logger.log('    Remove Label: ' + labels[l]);
                    }
                }

            }
        }
    }
}

//
// Date range helper function
// Returns today's date
//
function TODAY() {
    var today = new Date();
    var dd = today.getDate();
    var mm = today.getMonth() + 1; //January is 0!
    var yyyy = today.getFullYear();

    return { year: yyyy, month: mm, day: dd };
}

//
// Date range helper functions
// Returns date 90 days ago
//
function LAST_90_DAYS() {
    var date = new Date(); 
    date.setDate(date.getDate() - 90);
    
    var dd = date.getDate();
    var mm = date.getMonth()+1; //January is 0!
    var yyyy = date.getFullYear();
  
    return {year: yyyy, month: mm, day: dd};
  }

//
// Date range helper functions
// Returns date 1 year ago
//
function LAST_YEAR() {
    var today = TODAY();

    today.year = today.year - 1;
    return today;
}

The latest version of this script is available on GitHub here:
Google Ads Device Bid Adjustment Script

Install the script and have it run once per week.

Every week, the script will look at the past 365 days of data for each of your Search and Shopping campaigns, and make device bid adjustments based on conversion rates.

The script will raise and lower your device bid adjustments by up to 5% each week (you can change this value if you prefer bigger adjustments). You can also adjust the date range for which you want the script to run, and what the minimum number of conversions are required to make a change.

Additional Resources

Categories
Digital Marketing Facebook Ads Google Ads

Adjusting Campaign Budgets towards a Target ROAS

Pop Quiz!

You have been running a campaign for 7 days with a daily budget of $50. Over those 7 days you have spent $350 with an ROAS of 1500%. You are using a “Maximize ROAS” bid strategy, and want to hit an ROAS of 2000%.

What should your daily budget be?

Here is the formula:

Formula to adjust budget towards a target ROAS
Formula for adjusting a budget towards a target ROAS

This formula calculates what your daily spend should have been to hit your target ROAS. Although there are no guarantees that future performance will be the same as past performance, it does provide a baseline from which to work.

If you are running a smart bidding campaign in either Google Ads or Facebook Ads, and are aiming for a target Return on Ad Spend (ROAS), this is a great formula to use for adjusting your budgets.

The Answer

Sample Campaign
Total Cost$50×7 = $350
# of Days7
Current ROAS1500%
Target ROAS2000%

Using the formula above we get:

New Daily Budget = $350 ÷ 7 × 1500 ÷ 2000 = $37.50

By reducing our budget from $50 to $37.50, we should be in better shape to hit our target ROAS of 2000%.

Related Reading

Categories
Analytics Digital Marketing Google Ads

Essential Google Ads Custom Columns

Here are some incredibly useful custom columns that every account should have:

Column NameFormulaType
ROASConv. Value ÷ Cost%
Cost of Sales (COS)Cost ÷ Conv. Value%
% COS CPCConv. Value ÷ Clicks x %$
Optimistic
% COS CPC
(Conv. Value + (Conv. Value ÷ Conversions)) ÷ Clicks x %$

ROAS

Return On Ad Spend — It is surprising that this column doesn’t exist by default, since Google provides target ROAS based bidding strategies.

NameROAS
DescriptionReturn on Ad Spend
FormulaConv. Value ÷ Cost
Data FormatPercent (%) or Money ($)

This metric tells you how much sales are generated for each dollar in ad spend.

  • 100% ROAS: For every $1 spent, you generated $1 in revenue.
  • 200% ROAS: For every $1 spent, you generated $2 in revenue.

Many of the automated bidding strategies in Google Ads let you specify a target ROAS goal, so this custom column allows you to see at a glance if your campaigns, ad groups, and keywords are hitting your ROAS targets.

COS (Cost of Sales)

COS stands for Cost Of Sales

NameCOS
DescriptionCost of Sales
FormulaCost ÷ Conv. Value
Data FormatPercent (%)

This metric is the inverse of ROAS and it displays ad spend as a percentage of revenue. Cost of sales is a very intuitive metric when comparing against your profit margins. If you know your profit margins, then it’s easy to see when your Cost of Sales exceeds those margins, and when you start to lose money on every sale.

  • 20% Cost of Sales: For every $1 in revenue, we spent $0.20 on ads.

To convert between ROAS and COS use these formulas:

Cost of Sales (COS) = 1 / ROAS

Return on Ad Spend (ROAS) = 1 / COS

% COS CPC

What is the max Cost per Click bid that will keep me below the specified % Cost of Sales target.

Name5% COS CPC
DescriptionMaximum CPC to stay below 5% Cost of Sales
FormulaConv. Value ÷ Clicks x 0.05
Data FormatMoney ($)

If you use Manual CPC bidding for a campaign, creating a few of these columns for various profit margins will give you access to quick calculation for your max CPC bids.

Eg:

5% COSMax CPC bid to hit 5% cost of sales
35% COSMax CPC bid to hit 35% cost of sales
55% COSMax CPC bid to hit 55% cost of sales

The number of columns you create and the values you use will depends on your business and profit margins. Create one column for some typical cost targets.

Optimistic % COS CPC

What is the max Cost per Click bid that will keep me below the specified % Cost of Sales target, under the assumption that the next click will result in a sale

NameOptimistic 5% COS CPC
DescriptionMaximum CPC to stay below 5% Cost of Sales, assuming the next click will result in a sale
Formula(Conv. Value + (Conv. Value ÷ Conversions)) ÷ Clicks x 0.05

or use a harde-coded order value:
(Conv. Value + avg_order_value) ÷ Clicks x 0.05
Data FormatMoney ($)

This custom column is similar to the % COS CPC column above, except that it is optimistic in assuming that the “next click” will result in a sale, and includes the next sale’s revenue in the Max cost per click calculations.

You can either hard-code your average conversion value, or you can calculate it based on past conversion data.

This column is useful when you are starting a new campaign where you don’t have much conversion data yet, and you want to be optimistic with your bidding.

Further Reading

Categories
Digital Marketing Google Ads Shopify Shopping

Enhance Shopify’s Google Shopping feed

The following script will allow you to enhance Shopify’s Google Shopping app feed to include:

  • Additional Images by Variant: More control over which product images get associated with which variant.
  • Exclude Out of Stock items from Google Programs. If you are paying to advertise, make sure you are only paying to display products you can sell.
  • Exclude Low Stock variants from Google Programs. Exclude color ways that have low size options. Show other color options or products instead.
  • GTIN: Optionally set GTIN to be your barcode value
Sale annotation in Google Shopping
SALE labels and price markdown annotations usually only appear if you provide Google Shopping with both your regular price (compare_at_price) and your sale price.

To add the above functionality, you must create and upload a supplemental data feed to Google Merchant Center.

Step 1
Create a Supplemental Data Feed in Shopify

The first step is to create a data feed in Shopify. We can accomplish this by creating a custom Shopify Collection Template that will output XML data instead of HTML:

1. Create a new Collection Template called collection.google-update.liquid with the following code:

{% layout none %}<?xml version="1.0"?>
<rss xmlns:g="http://base.google.com/ns/1.0" version="2.0">

{% comment %}
Google Shopping / Merchant Center + Shopify Product Update Feed by Alex Czartoryski
https://business.czarto.com/2020/10/14/enhance-shopify-google-shopping/

This version: Aug 30, 2022
The latest version of this script available here:
https://github.com/Czarto/ShopifyScripts/blob/master/Templates/collection.google-feed-update.liquid

TODO: Test & handle products without variants
TODO: Test & hanlde products without color
TODO: Specify Sizes to never exclude. eg: Women's 7,8
{% endcomment %}

{% comment %} Settings {% endcomment %}
{%- assign exclude_unavailable_variants = true -%}
{%- assign exclude_variant_colors_with_limited_availability = false -%}
{%- assign ignore_x_smallest_sizes = 1 -%}
{%- assign ignore_x_largest_sizes = 1 -%}
{%- assign minimum_percentage_availability = 50 -%}
{%- assign filter_variantImages_byColor = false -%}
{%- assign use_barcode_as_gtin = false -%}

{%- comment -%}
TODO: Move this into a snippet and use capture to assign the variable
{%- endcomment -%}}
{%- case shop.currency -%}
{%- when 'USD' -%}{%- assign CountryCode = 'US' -%}
{%- when 'CAD' -%}{%- assign CountryCode = 'CA' -%}
{%- when 'GBP' -%}{%- assign CountryCode = 'GB' -%}
{%- when 'AUD' -%}{%- assign CountryCode = 'AU' -%}
{%- else -%}{%- assign CountryCode = 'US' -%}
{%- endcase -%}

<channel>
<title>{{ shop.name }} {{ collection.title | strip_html | strip_newlines | replace: '&', '&amp;' }}</title>
<link>{{ shop.url }}</link>
<description>{{ collection.description | strip_html | strip_newlines | replace: '&', '&amp;' }}</description>

{%- paginate collection.products by 1000 -%}
{%- for product in collection.products -%}

    {%- comment -%} Get color option {%- endcomment -%}
    {%- for option in product.options -%}
      {%- if option == 'Color' -%}{% capture option_color %}option{{ forloop.index }}{% endcapture %}{%- endif -%}
    {%- endfor -%}

    {%- comment -%} Make a list of Colors to exclude {%- endcomment -%}
    {%- assign colors_to_exclude = "" -%}
    {%- if exclude_variant_colors_with_limited_availability -%}
        {%- for color in product.options_by_name['Color'].values -%}
            {%- assign variants = product.variants | where: option_color, color -%}
            {%- assign variants_to_process_count = variants.size | minus:ignore_x_smallest_sizes | minus:ignore_x_largest_sizes -%}
            {%- assign available_count = 0 -%}
            {%- assign total_processed_count = 0 -%}
            {%- for variant in variants offset:ignore_x_smallest_sizes limit:variants_to_process_count -%}
                {%- assign total_processed_count = total_processed_count | plus:1 -%}
                {%- if variant.available -%}{%- assign available_count = available_count | plus:1 -%}{%- endif -%}
            {%- endfor -%}
            {%- if total_processed_count == 0 -%}
              {%- continue -%}
            {%- endif -%}
            {%- assign percentage_availability = available_count | times: 100.0 | divided_by: total_processed_count | round -%}
            {%- if percentage_availability < minimum_percentage_availability -%}
            {% capture colors_to_exclude %}{{colors_to_exclude}}#{{ color }}{%endcapture%}
            {%- endif -%}
        {%- endfor -%}
        {%- assign colors_to_exclude = colors_to_exclude | split: "#" -%}
    {%- endif -%}

    {%- for variant in product.variants -%}
<item>
    <g:item_group_id>shopify_{{ CountryCode }}_{{ product.id }}</g:item_group_id>
    <g:id>shopify_{{ CountryCode }}_{{ product.id }}_{{ variant.id }}</g:id>
    <g:mpn>{{ variant.sku }}</g:mpn>
    <g:barcode>{{ variant.barcode }}</g:barcode>
    {% if use_barcode_as_gtin %}<g:gtin>{{ variant.barcode }}</g:gtin>{% endif %}

    {%- comment -%} Additional Images by Color {%- endcomment -%}
    {%- assign additional_images = product.images -%}
    {%- for option in product.options -%}
    {%- if option == 'Color' -%}{% capture variant_color %}{{ variant.options[forloop.index0] }}{% endcapture %}{%- endif -%}
    {%- endfor -%}
    {% if filter_variantImages_byColor %}{% assign additional_images = product.images | where: "alt", variant_color | sort: 'attached_to_variant' | reverse%}{% endif %}
    {% if additional_images.size > 1 %}{%- for image in additional_images offset:1 limit:10 -%}
    <g:additional_image_link>https:{{ image.src | product_img_url: 'master' }}</g:additional_image_link>
    {% endfor %}{% endif %}

    {%- comment -%} Exclude Out of Stock Variants {%- endcomment -%}
    {% if exclude_unavailable_variants and variant.available == false %}
    <g:pause>ads</g:pause>
    {% elsif exclude_variant_colors_with_limited_availability and colors_to_exclude contains variant_color %}
    <g:pause>ads</g:pause>
    {% endif %}
</item>

  {% endfor %}
{% endfor %}
{% endpaginate %}
</channel>
</rss>

Available on github here

2. Create a new collection called “google-update” and choose google-update as your collection template.

3. Preview the collection and copy the url. Your url should look something like this: yourstoredomain.com/collections/google-update

Step 2
Configure your Feed

There are a few options you can configure in your feed, all located towards the top of your template file in the “configuration” section.

Exclude Out of Stock Variants
exclude_unavailable_variants

true
Exclude Limited Stock Color Variants
exclude_variant_colors_with_limited_availability
ignore_x_smallest_sizes
ignore_x_largest_sizes
minimum_percentage_availability

false
1
1
50
Filter Variant Images by Color + Alt Text Matching
filter_variantImages_byColor

false
Set GTIN to Variant’s Barcode Value
use_barcode_as_gtin

false

Exclude Out of Stock Variants

exclude_unavailable_variants = true

By default, any variants that are out of stock will be excluded from Google Shopping, Google Shopping Actions, and Dynamic Remarketing. Change the value of exclude_unavailable_variants = false if you want to disable this behaviour.

Exclude Limited Stock Color Variants

exclude_variant_colors_with_limited_availability = true

This setting is intended for apparel products, where you may have many colors of a product, but limited sizes available in a specific product. Setting this value to true will cause the script to attempt to exclude the colors with low availability (so that alternative colors or products can show instead).

There are a few additional configuration items for this setting that you can change:

minimum_percentage_availability
default = 50
Minimum % of sizes available before all variants of this color are excluded.
ignore_x_smallest_sizes
default = 1
Ignore the x smallest sizes in the % available calculation
ignore_x_largest_sizes
default = 1
Ignore the x largest sizes in the % available calculation

Filter Variant Images by Color
and Alt Text Matching

filter_variantImages_byColor = true

Setting this value to true will assign additional images to the current variant where the image’s Alt Text matches the variant’s color. Note that the primary variant’s image will always be included in the feed regardless of Alt text.

Set GTIN to Variant’s Barcode Value

use_barcode_as_gtin = true

Setting this value to true will assign your barcode value to GTIN. If you would like the option to set a different value than Barcode, it should be pretty straightforward to edit the code.

Step 3
Add a Supplemental Data Feed in Google Merchant Center

1. Open Merchant Center and go to
Products > Feeds > Supplemental Feeds > Add Supplemental Feed

NameFeed Update Script
Feed TypeScheduled Fetch
File Namegoogle-update
File Urlyourstoredomain.com/collections/google-update

Leave everything else as default values and click Continue

2. Make sure there’s a checkmark beside Content API and click Create Feed

3. You should now see your newly created feed in the Supplemental Feeds section. Click on your feed’s name and then click on Fetch Now to update your product data now.

Testing

It may take up to 30 minutes for your main feed to be updated. It is a good idea to review your products and feed to ensure that everything is coming through as expected, and tweak as required.

If everything looks good, your new sale pricing, variant images, and program availability should now be updated once per day.

Related Reading

Categories
Attribution Digital Marketing eCommerce Google Ads Shopify

Optimize Shopify’s Google Ads Conversion Tracking

Here are some changes to make to your Google Ads conversion tracking after you connect Shopify’s Google & YouTube Shopping Channel.

STEP 1
Install the Google Shopping Channel

This guide assumes that you have the Google Shopping Channel installed on your Shopify store. If this is not the case, then:

  1. Install the Google & YouTube Shopping Channel
  2. Connect your Google Account
  3. Turn on Conversion Tracking

STEP 2
Remove Old Conversion Tracking Code

By connecting Shopify to Google Ads via the Google Shopping Channel, Shopify will begin sending conversion data to your Google Ads account. If you were already tracking conversions in Google Ads, then you need to make sure you are not duplicating your conversion data:

  • If you previously had a Google Ads conversion tracking script installed in your checkout, then remove that code.
  • If you were importing conversions from Google Analytics, stop importing those conversions.

STEP 3
Conversion Windows and Attribution Models

Although Shopify created multiple conversion actions, you only need to worry about the Google Shopping App Purchase conversion event.

Click on the Google Shopping App Purchase conversion action and update the following settings:

Old ValueNew Value
CountEvery ConversionOne
View-through Conversion Window30 days1 day
Engaged-view conversion window3 days1 day
Attribution ModelLast clickData Driven

One Conversion per click

If you count “Every Conversion” you increases the risk of double attribution across your various channels.

For example: A user clicks on your Google ad and makes a purchase. It makes sense to count and attribute this conversion to Google Ads. If this same user later receives your newsletter, and purchases again, then you probably want to attribute that sale to the Newsletter and NOT to Google. Setting conversion count to “One” instead of “Every” ensures that only the first sale gets attributed to google, and not the second.

Counting “Every” conversion increases the risk of double attribution

Click-Through Conversions

Arguably, you should set your Click-Through Conversion window to 30 days. 90 days seems extremely long, and could increases your chances of double attribution across multiple sales channels.

However, if you are using Performance Max or Automateed Bidding, the bidding algorithm can only take into account conversions that have occurred within the specified conversion window. So theoretically, the longer your conversion window, the more data for Smart Bidding to optimize with. So the 90 day click-conversion window can probably stay.

View-Through Conversions

Set the View-Through Conversion window to only 1 day. A longer View-Through conversion window is dangerous, and will lead to over attributing sales to Display Remarketing, YouTube, and Performance Max. This will invariably cause you to overspend on those campaigns.

A View-Through conversion window greater than 1 day is dangerous, and will lead to over attributing sales to undeserving channels.

Further Reading

Categories
Digital Marketing Google Ads Shopify Shopping

Add Sale Price to Shopify’s Google Shopping feed

DEPRECATED: As of at least Aug 30, 2022, the defautlt Google sales channel properly sends the product’s sale price to Google. This post and code are no longer being maintained, and are here only for reference.

Sale annotation in Google Shopping
SALE labels and price markdown annotations usually only appear if you provide Google Shopping with both your regular price (compare_at_price) and your sale price.

Create a Price Feed in Shopify

The first step is to create a data feed in Shopify containing your products sale and regular prices. We can accomplish this by creating a custom Shopify Collection Template that will output XML data instead of HTML:

1. Create a new Collection Template

{% layout none %}<?xml version="1.0"?>
<rss xmlns:g="http://base.google.com/ns/1.0" version="2.0">
{% paginate collection.products by 1000 %}
{%- case shop.currency -%}
{%- when 'USD' -%}{%- assign CountryCode = 'US' -%}
{%- when 'CAD' -%}{%- assign CountryCode = 'CA' -%}
{%- when 'GBP' -%}{%- assign CountryCode = 'GB' -%}
{%- when 'AUD' -%}{%- assign CountryCode = 'AU' -%}
{%- else -%}{%- assign CountryCode = 'US' -%}
{%- endcase -%}
<channel>
<title>{{ shop.name }} {{ collection.title | strip_html | strip_newlines | replace: '&', '&amp;' }}</title>
<link>{{ shop.url }}</link>
<description>{{ collection.description | strip_html | strip_newlines | replace: '&', '&amp;' }}</description>
{% for product in collection.products %} 
  {% for variant in product.variants %}
    {%- if variant.compare_at_price > variant.price -%}
      {%- assign OnSale = true -%}
      {%- assign Price = variant.compare_at_price -%}
      {%- assign SalePrice = variant.price -%}
        <item>
            <g:item_group_id>shopify_{{ CountryCode }}_{{ product.id }}</g:item_group_id>
            <g:id>shopify_{{ CountryCode }}_{{ product.id }}_{{ variant.id }}</g:id>
            <g:price>{{ Price | money_without_currency }} {{ shop.currency }}</g:price>
            <g:sale_price>{{ SalePrice | money_without_currency }} {{ shop.currency }}</g:sale_price>
        </item>
    {%- endif -%}
{% endfor %}
{% endfor %}
</channel>
</rss>
{% endpaginate %}

Also available on github

2. Create a new collection called “google-feed-sale-price” based on your Collection Template

  • IMPORTANT Choose xml-pricing-feed as your collection template
  • Add products to the collection (or you can create an automatic collection with Compare At Price is Greater than 1)

3. Preview the collection and copy the url.

  • Your url should look something like this: yourstoredomain.com/collections/google-feed-sale-price
  • When you preview your page, it should look like a bunch of unformatted text on your page. If you see images, then you probably skipped the first bullet point in Step 2.

Add a Supplemental Data Feed in Google Merchant Center

4. Open Merchant Center and go to
Products > Feeds > Supplemental Feeds > Add Supplemental Feed

  • Name: Sale Pricing Update
  • Feed Type: Scheduled Fetch
  • File Name: google-feed-sale-price
  • File Url: yourstoredomain.com/collections/google-feed-sale-price

Leave everything else as default values and click Continue

5. Make sure there’s a checkmark beside Content API and click Create Feed

6. You should now see your newly created feed in the Supplemental Feeds section. Click on your feed’s name and then click on Fetch Now to update pricing data immediately.

Done

It may take up to 30 minutes for your main feed to be updated. Any new sale pricing will now be uploaded once per day.

Related Reading

Categories
Digital Marketing Google Ads

Essential Google Ads Remarketing Audiences

Below is a list of the Essential Google Audiences the every e-commerce site should create in their Google Ads Account. Even if you don’t plan on using them right away, creating them now will ensure that they can grow so they are ready to be used in the future.

Google’s Built-in Audiences

Before we create our own audiences, we first need to install the Google Ads Dynamic Remarketing Code. This triggers Google to automatically create some built-in Remarketing Audiences:

Audience NameDurationDescription
Shopping cart abandoners30 daysPeople who added products to the shopping cart in the past 30 days but did not complete the purchase
Product Viewers30 daysPeople who viewed specific product pages on your site in the past 30 days but did not create a shopping cart
Past buyer30 daysPeople who purchased products from you in the past 30 days
All visitors30 daysPeople who visited pages that contain your remarketing tags in the past 30 days
All converters180 daysPeople who converted on your site in the last 180 days. Based on your conversion tracking tag. This is not necessarily people who have purchased from you, but anyone who has triggered a “conversion”. (eg: Phone call from an ad)
General Visitors30 daysPeople who visited your website in the past 30 days but did not view any specific products

Additional Audiences

The additional audiences to create follow the same pattern used in Google’s built-in audiences, but with expanded membership durations. The main focus is on Shopping Cart Abandoners, Product Viewers, and Past Buyers.

Shopping Cart Abandoners

Google will have already created a “Shopping cart abandoners” audience with 30 day time window. We will create the following additional audiences for 7, 14, 90, and 180 day durations:

  • Shopping cart abandoners: 7d
  • Shopping cart abandoners: 14d
  • Shopping cart abandoners: 90d
  • Shopping cart abandoners: 180d
Audience Name:Shopping cart abandoners: xd
“x” will be the duration.
Eg: Shopping cart abandoners: 7d
List Members:Visitors of a page who did not visit another page
Visited page:URL contains cart
Unvisited pageURL contains thank_you
Membership Duration:7, 14, 90, 180 days
* 30 day duration is already created by Google

Past Buyers

Google will have already created a “Past buyers” audience with 30 day time window. We will create these additional audiences for 14, 90, 180, 365, and 520 day durations:

  • Past buyers: 14d
  • Past buyers: 90d
  • Past buyers: 180d
  • Past buyers: 365d
  • Past buyers: 520d
Audience NamePast buyers xd
“x” will be the duration.
Eg: Past buyers: 14d
List MembersVisitors of a page with specific tags
TagsPurchase
Membership duration14, 90, 180, 365, 520 days
* 30 day duration is already created by Google

Product Viewers

Google will have already created a “Product viewers” audience with a 30 day duration. We will create additional audiences with 14, 90, 180, 365, and 520 day durations.

  • Product viewers: 14d
  • Product viewers: 90d
  • Product viewers: 180d
  • Product viewers: 365d
  • Product viewers: 520d
Audience Name:Product viewers: xd
“x” will be the duration.
Eg: Product Viewers: 14d
List Members:Visitors of a page who did not visit another page
Visited page:URL contains product
Unvisited pageURL contains cart
Membership Duration:14, 90, 180, 365, 520 days
* 30 day duration is already created by Google

General Notes

  • The Membership Durations are somewhat arbitrary. You can get more or less granular, and set your own intervals. It is however best to start with something simple and add granularity later on.
  • A Remarketing Audience needs a minimum of 1,000 members to be eligible to serve. When creating your audience durations, consider how much time it will take to reach 1,000 memebers. eg: How long will it take before your site generates 1,000 abandoned carts, or 1,000 purchases? That will probably be the shortest duration with which you should start.
  • You should generally add all these lists as “Observations” to all your campaigns.

More Reading…

Categories
Attribution Digital Marketing Google Ads

YouTube Video Campaigns are Over-Reporting Conversions

YouTube Video Campaigns count Conversions very differently from other Campaign types

YouTube Video Campaigns count view-conversions as click-conversions, and include them in the “Conversions” columns of Google Ads.

If you are running a ROI focused YouTube Campaign that is targeting a lower funnel Remarketing Audience, the revenue and conversions reported by Google Ads can be over-inflated by 1000% or more vs what Google Analytics will report.

When running a YouTube Campaigns, always:

  1. Be suspicious and skeptical of the conversion data
  2. Adjust your performance targets. Conversions will be overinflated by a factor of up to 1000%, so adjust your performance accordingly.
  3. Use Google Analytics as your measure of YouTube Campaign performance
  4. Avoid Targeting lower funnel Remarketing Lists with YouTube. Save these lists for campaigns that offer true click-based conversion tracking.
  5. Explicitly exclude low funnel Remarketing Lists from your targeting. This will ensure the attribution poaching is limited.

YouTube Conversion Tracking Explained

Standard Campaign Behaviour

For most campaign types, a conversion is only recorded in the “Conversions” column when someone clicks on your ad and then proceeds to make a purchase. This is called a Click-Through Conversion. If there are no clicks on your ad, no conversion is recorded in the Conversions column.

This is the normal expected behaviour.

YouTube Campaign Behaviour

For YouTube campaigns, clicks behave normally: If someone clicks on your ad and then makes a purchase, that purchase is recorded as a conversion as expected. So far so good.

However, if someone DOES NOT CLICK on your ad, but watches your entire ad, and then later purchases, that purchase will be recorded as a conversion and attributed to YouTube.

As per Google:

A ‘view’ is counted when someone watches 30 seconds (or the whole ad if it’s shorter than 30 seconds) or clicks on a part of the ad. A ‘view’ that leads to a conversion is counted in the ‘Conversions’ column. 

Google Ads Help: Understand your conversion tracking data
https://support.google.com/google-ads/answer/6270625

Essentially, these are View-Through Conversions masquerading as Click-Through Conversions. The ad was never clicked, yet a conversion was recorded anyway. Considering that many ads are only 5 seconds long, most ad views are likely being treated as clicks.

This is NOT EXPECTED behaviour!

Attribution Poaching

Attribution Poaching is when one Channel tries to take credit for a sale that was either going to happen anyway, or for a sale that was actually caused by another Channel.

Consider this scenario

You create a YouTube campaign targeting your Shopping Cart Abandoners with a 5 second video ad.

One of your potential customers visits your site, and ads a product to their Shopping Cart. By adding to the Shopping Cart, they are now in your Shopping Cart Abandoners audience, and will be actively targeted by your YouTube Campaign.

They continue to browse your site, but they are interrupted by an e-mail or text from a friend with a link to a funny YouTube video. They click on the link and watch the video. While watching the video, they are forced to watch your 5s video ad (which they ignore). Since they watched y0ur entire ad, Google considers that a “click”.

They then eventually return to your site, to complete the purchase. Or maybe they complete the purchase 3 days later after receiving your Cart Abandonment e-mail.

In either case, your YouTube Campaign will claim credit for the Purchase, and will count it as a Conversion even thought that customer never clicked on your ad.

Why is Google Doing This?

A video view is much more like an impression than like a click, so why are these conversions being lumped in with click-through conversions?

The simple cynical answer is that Goolge makes more more money this way. By counting view-through conversions, the YouTube campaigns will appear to perform much better than they actually do with just click-through conversions (as much as 1000% better). If advertisers think that YouTube is performing 10X better, then they will allocated 10X more budget. The end result is Google makes 10X more money from YouTube.

A “Video View” is more like an Impressions than a Click

Video-View-Conversions should clearly be lumped into the View-Through Conversion column. If Google wants to explicitly report on Video-View-Conversions separately, then they should create another column type. Don’t lump them in with click-through conversions.

Unexpected Behaviour

The problem with all this is that suddenly the “Conversions” column behaves differently in one campaign type vs another. Suddenly the clean click-through conversion data is being polluted with View-Through conversion data. This makes YouTube campaigns appear to perform much better than they should. Which will lead you to incorrectly increase spend.

To make the matter worse…

  1. Many people have long click-through conversion windows. Often 30 days or even longer. This essentially allows a YouTube to claim credit for a conversion that happens 30 days after a video view.
  2. Many video ads are short – only 5s long. Most of these short ads are probably viewed in their entirety, meaning that they are all being counted as clicks. Often the user is forced to watch the entire 5s ad – again a click. This leads to more incorrectly attributed click-conversions.
  3. Often people target video ads using remarketing lists. Often the lowest hanging fruits for video campaigns are Remarketing Audiences. This shows ads to people who know your brand, who have recently visited your site, who may be subscribed to your newsletter, and who may in fact be currently actively shopping on your site. They are all likely to buy from you regardless of the the video ad, but the video ad will take credit for all their purchases.

This could also affect your Display and Discovery Campaigns

This issue could also affect your regular display campaigns and possibly your Discovery campaigns if they are serving ads on the YouTube network.

My recommendation for Display Campaigns is to try and exclude all YouTube placements: Go to: Campaign Settings > Additional Settings > Content Exclusions and select all of the following for exclusion:

  • Live streaming YouTube video
  • Embedded video
  • In-video

Notes:

  • This should only be an issue if you use Google Ads Native Conversion tracking. If you import your conversions directly from Google Analytics, then this should not be an issue (as Analytics only attributes conversions to the last click)
  • If you are running  a pure brand awareness campaign, this is probably less of a concern for you.
  • If you are an Ad Agency getting paid as a % of ad spend, then YouTube campaigns can make you a lot of money.

More Reading