diff --git a/config/install/dfp.settings.yml b/config/install/dfp.settings.yml
new file mode 100644
index 0000000..7d222a2
--- /dev/null
+++ b/config/install/dfp.settings.yml
@@ -0,0 +1,12 @@
+network_id: ''
+adunit_pattern: ''
+click_url: ''
+async_rendering: true
+disable_init_load: false
+single_request: true
+ad_categories_bundles: []
+default_slug: ''
+collapse_empty_divs: 1
+hide_slug: true
+targeting: []
+adtest_adunit_pattern: ''
diff --git a/config/install/taxonomy.vocabulary.forums.yml b/config/install/taxonomy.vocabulary.forums.yml
new file mode 100644
index 0000000..7d5011d
--- /dev/null
+++ b/config/install/taxonomy.vocabulary.forums.yml
@@ -0,0 +1,11 @@
+langcode: en
+status: true
+dependencies:
+  enforced:
+    module:
+      - dfp
+name: 'DFP Ad Categories'
+vid: dfp_ad_categories
+description: '@todo'
+hierarchy: 1
+weight: 0
diff --git a/config/schema/dfp.schema.yml b/config/schema/dfp.schema.yml
new file mode 100644
index 0000000..3569e88
--- /dev/null
+++ b/config/schema/dfp.schema.yml
@@ -0,0 +1,129 @@
+# Schema for the configuration files of the DFP module.
+
+dfp.settings:
+  type: config_object
+  label: 'DFP settings'
+  mapping:
+    network_id:
+      type: string
+      label: 'DFP network ID'
+    adunit_pattern:
+      type: string
+      label: 'Ad unit pattern'
+    click_url:
+      type: string
+      label: 'Click URL'
+    async_rendering:
+      type: boolean
+      label: 'Use asynchronous rendering'
+    disable_init_load:
+      type: boolean
+      label: 'Disable init load'
+    single_request:
+      type: boolean
+      label: 'Use a single request'
+    ad_categories_bundles:
+      type: ignore
+    default_slug:
+      type: string
+      label: 'The default slug'
+    collapse_empty_divs:
+      type: integer
+      label: 'Collapse empty divs'
+    hide_slug:
+      type: boolean
+      label: 'Whether to hide the slug'
+    targeting:
+      type: sequence
+      label: 'Targeting'
+      sequence:
+        type: mapping
+        mapping:
+          target:
+            type: string
+            label: 'Target'
+          value:
+            type: string
+            label: 'Value'
+    adtest_adunit_pattern:
+      type: string
+      label: 'Ad unit test pattern'
+
+dfp.tag.*:
+  type: config_entity
+  label: 'DFP tag'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    slot:
+      type: string
+      label: 'Ad slot'
+    size:
+      type: string
+      label: 'Ad size(s)'
+    adunit:
+      type: string
+      label: 'Ad unit'
+    slug:
+      type: string
+      label: 'Slug'
+    block:
+      type: boolean
+      label: 'Provide a block'
+    short_tag:
+      type: boolean
+      label: 'Display as a short tag'
+    breakpoints:
+      type: sequence
+      label: 'Breakpoints'
+      sequence:
+        type: mapping
+        mapping:
+          browser_size:
+            type: string
+            label: 'Browser size'
+          ad_sizes:
+            type: string
+            label: 'Ad size(s)'
+    targeting:
+      type: sequence
+      label: 'Targeting'
+      sequence:
+        type: mapping
+        mapping:
+          target:
+            type: string
+            label: 'Target'
+          value:
+            type: string
+            label: 'Value'
+    adsense_backfill:
+      type: mapping
+      label: 'Adsense backfill'
+      mapping:
+        ad_types:
+          type: string
+          label: 'Adsense ad types'
+        channel_ids:
+          type: string
+          label: 'Adsense channel IDs'
+        color:
+          type: mapping
+          label: 'Adsense color settings'
+          mapping:
+            background:
+              type: string
+              label: 'Background color'
+            border:
+              type: string
+              label: 'Border color'
+            link:
+              type: string
+              label: 'Link color'
+            text:
+              type: string
+              label: 'Text color'
+            url:
+              type: string
+              label: 'URL color'
diff --git a/dfp.admin.css b/css/dfp.admin.css
similarity index 100%
rename from dfp.admin.css
rename to css/dfp.admin.css
diff --git a/dfp.admin.inc b/dfp.admin.inc
deleted file mode 100644
index 34ca6e7..0000000
--- a/dfp.admin.inc
+++ /dev/null
@@ -1,702 +0,0 @@
-<?php
-
-/**
- * @file
- * Admin forms and functinality for DFP ads.
- */
-
-/**
- * Form builder for the global DFP settings form.
- */
-function dfp_admin_settings($form, $form_state) {
-  // Make sure that all the necessary files are included even on ajax callbacks.
-  form_load_include($form_state, 'inc', 'dfp', 'plugins/export_ui/dfp_ctools_export_ui');
-
-  $form['settings'] = array(
-    '#type' => 'vertical_tabs',
-  );
-
-  $form['#validate'][] = 'dfp_admin_form_validate';
-  $form['#submit'][] = 'dfp_admin_form_submit';
-
-  // Default tag settings.
-  $form['global_tag_settings'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Global Tag Settings'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#group' => 'settings',
-    '#attached' => array(
-      'js' => array(
-        'vertical-tabs' => drupal_get_path('module', 'dfp') . '/dfp.admin.js',
-      ),
-    ),
-  );
-  $form['global_tag_settings']['dfp_network_id'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Network ID'),
-    '#default_value' => variable_get('dfp_network_id', ''),
-    '#required' => TRUE,
-    '#description' => t('The Network ID to use on all tags. According to Google this value should begin with a /.'),
-  );
-  $form['global_tag_settings']['dfp_default_adunit'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Default Ad Unit Pattern'),
-    '#required' => FALSE,
-    '#maxlength' => 255,
-    '#default_value' => variable_get('dfp_default_adunit', ''),
-    '#description' => t('Use the tokens below to define how the default ad unit should display. This can be overridden on each tag. The network id will be included automatically. Example: [current-page:url:args:value:0]/[current-page:url:args:value:1]/[dfp_tag:slot]'),
-  );
-  $form['global_tag_settings']['dfp_click_url'] = array(
-    '#type' => 'textfield',
-    '#title' => t('DFP Click URL (Sync mode only)'),
-    '#default_value' => variable_get('dfp_click_url', ''),
-    '#description' => t('Sets a click URL on each DFP tag. Useful for intercepting ad click calls for reporting. Values beginning with http:// are treated as absolute; otherwise relative to current site domain.'),
-    '#states' => array(
-      'enabled' => array(
-        'input[name="dfp_async_rendering"]' => array('checked' => FALSE),
-      ),
-    ),
-  );
-  $form['global_tag_settings']['tokens'] = array(
-    '#theme' => 'token_tree',
-    '#token_types' => array('dfp_tag', 'node', 'term', 'user'),
-    '#global_types' => TRUE,
-    '#click_insert' => TRUE,
-    '#dialog' => TRUE,
-  );
-  $form['global_tag_settings']['dfp_async_rendering'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Render ads asynchronously'),
-    '#default_value' => variable_get('dfp_async_rendering', 1),
-    '#description' => t('This can speed up page rendering time by loading page content without waiting for ads to load.'),
-  );
-  $form['global_tag_settings']['dfp_disable_init_load'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Disable initial ad load'),
-    '#default_value' => variable_get('dfp_disable_init_load', 0),
-    '#description' => t('(Async mode only) Disables the initial fetch of ads from Google when the page is first loaded. Calls to refresh() can still be used to fetch ads.'),
-     '#states' => array(
-      'enabled' => array(
-        'input[name="dfp_async_rendering"]' => array('checked' => TRUE),
-      ),
-    ),
-  );
-  $form['global_tag_settings']['dfp_single_request'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Combine all ad requests into a single request'),
-    '#default_value' => variable_get('dfp_single_request', 1),
-    '#description' => t('This can speed up page rendering time by limiting the number of external requests.'),
-  );
-  $form['global_tag_settings']['dfp_enable_ad_categories'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Enable DFP Ad Categories'),
-    '#default_value' => variable_get('dfp_enable_ad_categories', 0),
-    '#description' => t('Example: if you have an "animals" vocabulary and you want to target the same ads to "dogs", "cats" and "hamsters" you can edit each of those terms and set the DFP Ad Category to "pets". Whenever the taxonomy terms are included as targeting values, anything tagged "cats" will target "pets" instead.'),
-  );
-  if (!module_exists('taxonomy')) {
-    $form['global_tag_settings']['dfp_enable_ad_categories']['disabled'] = TRUE;
-    $form['global_tag_settings']['dfp_enable_ad_categories']['#default_value'] = 0;
-    $form['global_tag_settings']['dfp_enable_ad_categories']['#description'] .= t('This requires the taxonomy module which is currently disabled.');
-  }
-  $bundles = field_info_bundles('taxonomy_term');
-  $options = array();
-  foreach ($bundles as $key => $value) {
-    if ($key != 'dfp_ad_categories') {
-      $options[$key] = $value['label'];
-    }
-  }
-  $form['global_tag_settings']['dfp_enable_ad_categories_bundles'] = array(
-    '#type' => 'checkboxes',
-    '#options' => $options,
-    '#prefix' => '<div style="margin-left: 20px">',
-    '#suffix' => '</div>',
-    '#default_value' => variable_get('dfp_enable_ad_categories_bundles', array()),
-    '#description' => t('Select the vocabularies on which DFP Ad Categories should be enabled.'),
-    '#states' => array(
-      'visible' => array(
-        'input[name="dfp_enable_ad_categories"]' => array('checked' => TRUE),
-      ),
-    ),
-  );
-  $form['global_tag_settings']['dfp_token_cache_lifetime'] = array(
-    '#type' => 'textfield',
-    '#title' => t("Token cache lifetime"),
-    '#default_value' => variable_get("dfp_token_cache_lifetime", 0),
-    '#description' => t('The time, in seconds, that the DFP token cache will be valid for. The token cache will always be cleared at the next system cron run after this time period, or when this form is saved.'),
-  );
-
-
-  // Global display options.
-  $form['global_display_options'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Global Display Options'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#group' => 'settings',
-  );
-  $form['global_display_options']['dfp_default_slug'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Global Slug'),
-    '#default_value' => variable_get('dfp_default_slug', ''),
-    '#required' => FALSE,
-    '#description' => t('Slug all ad tags with this label. Example: Advertisement', array('@tag' => '<none>')),
-  );
-  $form['global_display_options']['dfp_collapse_empty_divs'] = array(
-    '#type' => 'radios',
-    '#title' => t('Collapse empty divs'),
-    '#default_value' => variable_get('dfp_collapse_empty_divs', 1),
-    '#options' => array(
-      0 => t('Never'),
-      1 => t('Collapse only if no ad is served'),
-      2 => t('Expand only if an ad is served'),
-    ),
-    '#description' => t('<dl><dt>Never</dt><dd>Never collapse ad slots.</dd><dt>Collapse only</dt><dd>Collapse before any ad is loaded. Useful if ad slots will get filled most of the time.</dd><dt>Expand only</dt><dd>Collapse all divs on the page before any ads are fetched and expand if an ad is loaded into the ad slot. Useful if ad slots will stay empty most of the time.</dd></dl>'),
-  );
-  $form['global_display_options']['dfp_slug_placement'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Hide slug if no ad is served (recommended)'),
-    '#default_value' => variable_get('dfp_slug_placement', 1),
-    '#states' => array(
-      'visible' => array(
-        'input[name="dfp_collapse_empty_divs"]' => array('!value' => 0),
-      ),
-    ),
-  );
-
-  // Global targeting options.
-  $form['targeting_settings'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Global Targeting'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#group' => 'settings',
-  );
-  $existing_targeting = _dfp_targeting_get_existing($form_state, variable_get('dfp_targeting', array()));
-  _dfp_targeting_form($form['targeting_settings'], $existing_targeting);
-
-  // AdTest Settings
-  $form['adtest'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Ad Test Settings'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#group' => 'settings',
-  );
-  $form['adtest']['dfp_adtest_adunit'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Ad Unit Pattern for Ad Tests'),
-    '#description' => t('Override the Ad Unit value for all the ad tags on a page by adding ?adtest=true to the URL. Use the tokens below to define how the ad unit should display. Example: [dfp_tag:network_id]/test/[dfp_tag:slot]'),
-    '#default_value' => variable_get('dfp_adtest_adunit', ''),
-  );
-  $form['adtest']['tokens'] = array(
-    '#theme' => 'token_tree',
-    '#token_types' => array('dfp_tag', 'node', 'term', 'user'),
-    '#global_types' => TRUE,
-    '#click_insert' => TRUE,
-    '#dialog' => TRUE,
-  );
-
-  // Javascript.
-  $form['javascript'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Inject Javascript'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#weight' => 20,
-    '#group' => 'settings',
-  );
-  $form['javascript']['dfp_injected_js'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Inject javascript') . ' 1',
-    '#description' => t('Inject this javascript into the @tag on every page request immediately after the googletag object becomes available.', array('@tag' => '<head>')),
-    '#default_value' => variable_get('dfp_injected_js', ''),
-    '#rows' => 5,
-  );
-  $form['javascript']['dfp_injected_js2'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Inject javascript') . ' 2',
-    '#description' => t('Inject this javascript into the @tag on every page request immediately before the enableServices call.', array('@tag' => '<head>')),
-    '#default_value' => variable_get('dfp_injected_js2', ''),
-    '#rows' => 5,
-  );
-
-  return system_settings_form($form);
-}
-
-/**
- * Submit handler for the global DFP settings form.
- */
-function dfp_admin_form_validate($element, &$form_state)
-{
-  $settings = $element['global_tag_settings'];
-  if (!empty($settings['dfp_click_url']['#value']) && $settings['dfp_async_rendering']['#value'] == 1) {
-    form_error($settings['dfp_click_url'], t('Setting a click URL does not work with async rendering.'));
-  }
-}
-
-/**
- * Submit handler ensures that targeting values are saved with the "dfp" prefix.
- */
-function dfp_admin_form_submit($form, &$form_state) {
-  // Since the targeting form is reusable it isn't already in the settings
-  // array so we grab it here.
-  $form_state['values']['dfp_targeting'] = $form_state['values']['targeting'];
-  unset($form_state['values']['targeting']);
-
-  // Attach (or delete) an instance of the dfp_ad_categories term_reference
-  // field for each vocabulary that should (or should not) have DFP Ad
-  // Categories enabled.
-  foreach ($form_state['values']['dfp_enable_ad_categories_bundles'] as $bundle => $enable) {
-    $existing_instance = field_read_instance('taxonomy_term', 'field_dfp_ad_categories', $bundle);
-    $enable = $enable && !$existing_instance && $form_state['values']['dfp_enable_ad_categories'];
-    if ($enable) {
-      $instance = array(
-        'field_name' => 'field_dfp_ad_categories',
-        'entity_type' => 'taxonomy_term',
-        'label' => t('DFP Ad Category'),
-        'bundle' => $bundle,
-        'required' => FALSE,
-        'widget' => array(
-          'type' => 'options_select'
-        ),
-      );
-      field_create_instance($instance);
-    }
-    elseif (!$enable && $existing_instance) {
-      // Delete this field instance, but be certain not to delete the field.
-      field_delete_instance($existing_instance, FALSE);
-    }
-  }
-}
-
-/**
- * Validation function used by an individual target in the targeting form.
- */
-function dfp_target_form_validate($element, &$form_state) {
-  if (empty($element['target']['#value']) && !empty($element['value']['#value'])) {
-    form_error($element['target'], t('The target cannot be empty if a value exists.'));
-  }
-  elseif (!empty($element['target']['#value']) && empty($element['value']['#value'])) {
-    form_error($element['value'], t('The value cannot be empty if a target exists.'));
-  }
-}
-
-/**
- * Validation function used by the targeting form.
- */
-function dfp_targeting_form_validate(&$element, &$form_state) {
-  if ($form_state['triggering_element']['#value'] != t('Add another target')) {
-    dfp_targeting_trim($form_state['values']);
-  }
-}
-
-/**
- * Helper function that takes a form_state['values'] and removes empty targets.
- */
-function dfp_targeting_trim(&$values, $parent = 'targeting') {
-  foreach ($values as $key => &$val) {
-    if ($key === $parent) {
-      // We found the targeting values.
-      foreach ($val as $k => $v) {
-        if (empty($val[$k]['target']) && empty($val[$k]['value'])) {
-          unset($val[$k]);
-        }
-      }
-      // Reset the array indexes to prevent wierd behavior caused by a target
-      // being removed in the middle of the array.
-      $val = array_values($val);
-      break;
-    }
-    elseif (is_array($val)) {
-      dfp_targeting_trim($val, $parent);
-    }
-  }
-}
-
-/**
- * Submit handler to add more targets to an ad tag.
- */
-function dfp_more_targets_submit($form, &$form_state) {
-  $form_state['targeting'] = $form_state['input']['targeting'];
-  $form_state['rebuild'] = TRUE;
-}
-
-/**
- * Ajax callback for adding targets to the targeting form.
- */
-function dfp_more_targets_js($form, $form_state) {
-  return $form['targeting_settings']['targeting'];
-}
-
-/**
- * Helper form builder for the targeting form.
- */
-function _dfp_targeting_form(&$targeting_form, $existing_targeting = array()) {
-  // Display settings.
-  $targeting_form['targeting'] = array(
-    '#type' => 'markup',
-    '#tree' => FALSE,
-    '#prefix' => '<div id="dfp-targeting-wrapper">',
-    '#suffix' => '</div>',
-    '#theme' => 'dfp_target_settings',
-    '#element_validate' => array('dfp_targeting_form_validate'),
-  );
-
-  // Add existing targets to the form unless they are empty.
-  foreach ($existing_targeting as $key => $data) {
-    _dfp_target_form($targeting_form, $key, $data);
-  }
-  // Add one blank set of target fields.
-  _dfp_target_form($targeting_form, count($existing_targeting));
-
-  $targeting_form['targeting']['dfp_more_targets'] = array(
-    '#type' => 'submit',
-    '#value' => t('Add another target'),
-    '#submit' => array('dfp_more_targets_submit'),
-    '#limit_validation_errors' => array(),
-    '#ajax' => array(
-      'callback' => 'dfp_more_targets_js',
-      'wrapper' => 'dfp-targeting-wrapper',
-      'effect' => 'fade',
-      'file' => 'plugins/export_ui/dfp_ctools_export_ui.inc',
-    ),
-  );
-  $targeting_form['tokens'] = array(
-    '#theme' => 'token_tree',
-    '#token_types' => array('dfp_tag', 'node', 'term', 'user'),
-    '#global_types' => TRUE,
-    '#click_insert' => TRUE,
-    '#dialog' => TRUE,
-  );
-}
-
-/**
- * Helper form builder for an individual target.
- */
-function _dfp_target_form(&$form, $key, $data = array()) {
-  $form['targeting'][$key] = array(
-    '#prefix' => '<div class="target" id="target-' . $key . '">',
-    '#suffix' => '</div>',
-    '#element_validate' => array('dfp_target_form_validate'),
-  );
-  $form['targeting'][$key]['target'] = array(
-    '#type' => 'textfield',
-    '#title_display' => 'invisible',
-    '#title' => t('Target Name'),
-    '#size' => 10,
-    '#default_value' => isset($data['target']) ? $data['target'] : '',
-    '#parents' => array('targeting', $key, 'target'),
-    '#attributes' => array('class' => array('field-target-target')),
-  );
-  $form['targeting'][$key]['value'] = array(
-    '#type' => 'textfield',
-    '#title_display' => 'invisible',
-    '#title' => t('Target Value'),
-    '#size' => 20,
-    '#default_value' => isset($data['value']) ? $data['value'] : '',
-    '#parents' => array('targeting', $key, 'value'),
-    '#attributes' => array('class' => array('field-target-value')),
-  );
-  if (empty($data)) {
-    $form['targeting'][$key]['target']['#description'] = t('Example: color');
-    $form['targeting'][$key]['value']['#description'] = t('Example: red,white,blue');
-  }
-}
-
-/**
- * Returns the current targets. The default value will be used unless an "input"
- * exists in the form_state variable, in which case that will be used.
- */
-function _dfp_targeting_get_existing($form_state, $default = array()) {
-  $targeting = array();
-  if (isset($form_state['input']['targeting'])) {
-    $targeting = $form_state['input']['targeting'];
-  }
-  elseif (!empty($default)) {
-    $targeting = $default;
-  }
-
-  return $targeting;
-}
-
-/**
- * Helper function to remove empty target values from a given array.
- */
-function _dfp_targeting_remove_empty(&$target) {
-  foreach ($target as $key => $val) {
-    if (empty($val['target']) && empty($val['value'])) {
-      unset($target[$key]);
-    }
-  }
-  // Reset the target array's indexes to prevents strange behavior when users
-  // leave a blank row in the targeting form followed by a filled-in row. In
-  // this case clicking "Add another target" will not work as expected.
-  $target = array_values($target);
-}
-
-/**
- * Validation function used by an individual breakpoint in the breakpoints form.
- */
-function dfp_breakpoint_form_validate($element, &$form_state) {
-  if (empty($element['browser_size']['#value']) && !empty($element['ad_sizes']['#value'])) {
-    form_error($element['browser_size'], t('The browser size cannot be empty if ad size(s) exists.'));
-  }
-  elseif (!empty($element['browser_size']['#value']) && empty($element['ad_sizes']['#value'])) {
-    form_error($element['ad_sizes'], t('The ad size(s) cannot be empty if a browser size exists.'));
-  }
-  if (!empty($element['browser_size']['#value']) && !empty($element['ad_sizes']['#value'])) {
-    if (preg_match('/[^x|0-9]/', $element['browser_size']['#value'])) {
-      form_error($element['browser_size'], t('The browser size can only contain numbers and the character x.'));
-    }
-    elseif (preg_match('/[^x|,|0-9]/', $element['ad_sizes']['#value'])) {
-      form_error($element['ad_sizes'], t('The ad size(s) can only contain numbers, the character x and commas.'));
-    }
-  }
-}
-
-/**
- * Validation function used by the breakpoints form.
- */
-function dfp_breakpoints_form_validate(&$element, &$form_state) {
-  if ($form_state['triggering_element']['#value'] != t('Add another breakpoint')) {
-    dfp_breakpoints_trim($form_state['values']);
-  }
-}
-
-/**
- * Helper function that takes a form_state['values'] and removes
- * empty breakpoints.
- */
-function dfp_breakpoints_trim(&$values, $parent = 'breakpoints') {
-  foreach ($values as $key => &$val) {
-    if ($key === $parent) {
-      // We found the browser_size values.
-      foreach ($val as $k => $v) {
-        if (empty($val[$k]['browser_size']) && empty($val[$k]['ad_sizes'])) {
-          unset($val[$k]);
-        }
-      }
-      // Reset the array indexes to prevent wierd behavior caused by a
-      // breakpoint being removed in the middle of the array.
-      $val = array_values($val);
-      break;
-    }
-    elseif (is_array($val)) {
-      dfp_breakpoints_trim($val, $parent);
-    }
-  }
-}
-
-/**
- * Submit handler to add more breakpoints to an ad tag.
- */
-function dfp_more_breakpoints_submit($form, &$form_state) {
-  $form_state['breakpoints'] = $form_state['input']['breakpoints'];
-  $form_state['rebuild'] = TRUE;
-}
-
-/**
- * Ajax callback for adding breakpoints to the breakpoint form.
- */
-function dfp_more_breakpoints_js($form, $form_state) {
-  return $form['breakpoint_settings']['breakpoints'];
-}
-
-/**
- * Helper form builder for the breakpoints form.
- */
-function _dfp_breakpoints_form(&$breakpoints_form, $existing_breakpoints = array()) {
-  // Display settings.
-  $breakpoints_form['breakpoints'] = array(
-    '#type' => 'markup',
-    '#tree' => FALSE,
-    '#prefix' => '<div id="dfp-breakpoints-wrapper">',
-    '#suffix' => '</div>',
-    '#theme' => 'dfp_breakpoint_settings',
-    '#element_validate' => array('dfp_breakpoints_form_validate'),
-  );
-
-  // Add existing breakpoints to the form unless they are empty.
-  foreach ($existing_breakpoints as $key => $data) {
-    _dfp_breakpoint_form($breakpoints_form, $key, $data);
-  }
-  // Add one blank set of breakpoint fields.
-  _dfp_breakpoint_form($breakpoints_form, count($existing_breakpoints));
-
-  $breakpoints_form['breakpoints']['dfp_more_breakpoints'] = array(
-    '#type' => 'submit',
-    '#value' => t('Add another breakpoint'),
-    '#submit' => array('dfp_more_breakpoints_submit'),
-    '#limit_validation_errors' => array(),
-    '#ajax' => array(
-      'callback' => 'dfp_more_breakpoints_js',
-      'wrapper' => 'dfp-breakpoints-wrapper',
-      'effect' => 'fade',
-      'file' => 'plugins/export_ui/dfp_ctools_export_ui.inc',
-    ),
-  );
-}
-
-/**
- * Helper form builder for an individual breakpoint.
- */
-function _dfp_breakpoint_form(&$form, $key, $data = array()) {
-  $form['breakpoints'][$key] = array(
-    '#prefix' => '<div class="breakpoint" id="breakpoint-' . $key . '">',
-    '#suffix' => '</div>',
-    '#element_validate' => array('dfp_breakpoint_form_validate'),
-  );
-  $form['breakpoints'][$key]['browser_size'] = array(
-    '#type' => 'textfield',
-    '#title_display' => 'invisible',
-    '#title' => t('Minimum Browser Size'),
-    '#size' => 10,
-    '#default_value' => isset($data['browser_size']) ? $data['browser_size'] : '',
-    '#parents' => array('breakpoints', $key, 'browser_size'),
-    '#attributes' => array('class' => array('field-breakpoint-browser-size')),
-  );
-  $form['breakpoints'][$key]['ad_sizes'] = array(
-    '#type' => 'textfield',
-    '#title_display' => 'invisible',
-    '#title' => t('Ad Sizes'),
-    '#size' => 20,
-    '#default_value' => isset($data['ad_sizes']) ? $data['ad_sizes'] : '',
-    '#parents' => array('breakpoints', $key, 'ad_sizes'),
-    '#attributes' => array('class' => array('field-breakpoint-ad-sizes')),
-  );
-  $form['breakpoints'][$key]['delete_breakpoint'] = array(
-    '#type' => 'textfield',
-    '#title_display' => 'invisible',
-    '#title' => t('Ad Sizes'),
-    '#size' => 20,
-    '#default_value' => isset($data['ad_sizes']) ? $data['ad_sizes'] : '',
-    '#parents' => array('breakpoints', $key, 'ad_sizes'),
-    '#attributes' => array('class' => array('field-breakpoint-ad-sizes')),
-  );
-  if (empty($data)) {
-    $form['breakpoints'][$key]['browser_size']['#description'] = t('Example: 1024x768');
-    $form['breakpoints'][$key]['ad_sizes']['#description'] = t('Example: 300x600,300x250');
-  }
-}
-
-/**
- * Returns the current breakpoints. The default value will be used unless an
- * "input" exists in the form_state variable, in which case that will be used.
- */
-function _dfp_breakpoints_get_existing($form_state, $default = array()) {
-  $breakpoints = array();
-  if (isset($form_state['input']['breakpoints'])) {
-    $breakpoints = $form_state['input']['breakpoints'];
-  }
-  elseif (!empty($default)) {
-    $breakpoints = $default;
-  }
-
-  return $breakpoints;
-}
-
-/**
- * Theme function for the "breakpoint" form.
- */
-function theme_dfp_breakpoint_settings($variables) {
-  $form = $variables['form'];
-
-  $more_button = drupal_render($form['dfp_more_breakpoints']);
-  unset($form['dfp_more_breakpoints']);
-
-  $headers = array(t('Browser Size'), t('Ad Size(s)'));
-  $rows = array();
-  foreach (element_children($form) as $key) {
-    $rows[] = array(drupal_render($form[$key]['browser_size']), drupal_render($form[$key]['ad_sizes']));
-  }
-  $rows[] = array(array('data' => 'These breakpoints are set to implement DFP responsive mappings. See <a href="https://support.google.com/dfp_premium/answer/3423562?hl=en">this support article</a> for more information.', 'colspan' => 2));
-  return theme('table', array('header' => $headers, 'rows' => $rows)) . $more_button;
-}
-
-
-/**
- * Helper function returns the a value from a givne multidimensional array given
- * a specific key. If the key exists in more than one place, the first occurance
- * is used.
- */
-function _dfp_get_form_item($needle_key, $array) {
-  $children = element_children($array);
-
-  foreach ($children as $child) {
-    if ($child == $needle_key) {
-      return $array[$child];
-    }
-    elseif (is_array($array[$child])) {
-      $result = _dfp_get_form_item($needle_key, $array[$child]);
-      if ($result) {
-        return $result;
-      }
-    }
-  }
-
-  return FALSE;
-}
-
-/**
- * Theme function for the "target" form.
- */
-function theme_dfp_target_settings($variables) {
-  $form = $variables['form'];
-
-  $more_button = drupal_render($form['dfp_more_targets']);
-  unset($form['dfp_more_targets']);
-
-  $headers = array(t('Target'), t('Value(s)'));
-  $rows = array();
-  foreach (element_children($form) as $key) {
-    $rows[] = array(drupal_render($form[$key]['target']), drupal_render($form[$key]['value']));
-  }
-
-  return theme('table', array('header' => $headers, 'rows' => $rows)) . $more_button;
-}
-
-/**
- * Theme function for the "target" form.
- */
-function theme_dfp_adsense_color_settings($variables) {
-  $form = $variables['form'];
-
-  $caption = t('AdSense Color Settings');
-  $header = array(t('Color Setting'), t('Hex color value'), t('Sample'));
-  $rows = array();
-  foreach (element_children($form) as $key) {
-    $rows[] = array(
-      $form[$key]['#title'],
-      drupal_render($form[$key]),
-      "<div class='color-sample' style='background-color: #" . $form[$key]['#default_value'] . ";'>&nbsp;</div>",
-    );
-  }
-
-  $output = '  <label>' . t('Color Settings for Text Ads') . '</label>';
-  $output .= theme('table', array('attributes' => array('id' => 'color-settings'), 'header' => $header, 'rows' => $rows));
-
-  return $output;
-}
-
-/**
- * Theme function for the "size" context reaction form.
- */
-function theme_dfp_size_settings($variables) {
-  $form = $variables['form'];
-
-  $headers = array(t('Ad Slot'), t('Size Override'));
-  $rows = array();
-  $caption = t('You can override the size(s) of the ad slot for this context by entering the new size(s) below. Leave the field blank if the size should not be overridden.');
-
-  $children = element_children($form);
-  $last = array_pop($children);
-
-  foreach (element_children($form) as $key) {
-    if ($key != $last) {
-      unset($form[$key]['#description']);
-    }
-    $rows[] = array($form[$key]['#title'], drupal_render($form[$key]));
-  }
-  return theme('table', array('header' => $headers, 'rows' => $rows, 'caption' => $caption));
-}
diff --git a/dfp.api.php b/dfp.api.php
index 6170158..09830ce 100644
--- a/dfp.api.php
+++ b/dfp.api.php
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * Hooks provided by the Chaos Tool Suite.
+ * Hooks provided by the DFP module.
  *
  * This file is divided into static hooks (hooks with string literal names) and
  * dynamic hooks (hooks with pattern-derived string names).
@@ -14,57 +14,41 @@
  */
 
 /**
- * Alter the raw tag object just after it is loaded from the database.
+ * Alter a targeting key|value pair from a DFP tag.
  *
- * @param object $tag
+ * @param array $targeting
+ *   The DFP tag's targeting key|value pair.
  */
-function hook_dfp_tag_load_alter(&$tag) {
-
-}
-
-/**
- * Alter the tag object just after it is loaded and the settings have been
- * loaded properly.
- *
- * @param object $tag
- */
-function hook_dfp_tag_alter(&$tag) {
-
-}
-
-/**
- * Alter a targeting key|value pair.
- *
- * @param array $target
- */
-function hook_dfp_target_alter(&$target) {
-
+function hook_dfp_target_alter(&$targeting) {
+  // @todo Add example.
 }
 
 /**
  * Alter the global targeting key|value pairs.
  *
- * @param array $target
+ * @param array $targeting
+ *   The global targeting key|value pairs.
  */
-function hook_dfp_global_targeting_alter(&$target) {
+function hook_dfp_global_targeting_alter(&$targeting) {
+  // @todo Fix example to be Drupal 8 relevant.
   // The following example adds the URL arguments array to the targeting.
   $arg = arg();
   if (!empty($arg)) {
-    $targeting[] = array(
+    $targeting[] = [
       'target' => 'arg',
       'value' => $arg,
-    );
+    ];
   }
 }
 
 /**
- * Alter the keyvals array that is about to be used during the construction of
- * a short tag.
+ * Alters the key values array used during the construction of a short tag.
  *
- * @param array $keyvals
+ * @param array $key_values
+ *   The key values array used during the construction of a short tag.
  */
-function hook_dfp_short_tag_keyvals_alter(&$keyvals) {
-
+function hook_dfp_short_tag_keyvals_alter(&$key_values) {
+  // @todo Add example.
 }
 
 /**
diff --git a/dfp.info b/dfp.info
deleted file mode 100755
index 184e29e..0000000
--- a/dfp.info
+++ /dev/null
@@ -1,22 +0,0 @@
-name = Doubleclick for Publishers
-description = Integrates DFP Google Publisher Tags onto a site.
-core = 7.x
-package = Advertising
-
-configure = admin/structure/dfp_ads/settings
-
-;These version dependencies are still accurate but they have been commented out
-;to allow testbot to work properly.
-;dependencies[] = system (>= 7.17)
-;dependencies[] = ctools (>= 7.x-1.0-rc2)
-
-dependencies[] = system
-dependencies[] = ctools
-dependencies[] = taxonomy
-dependencies[] = entity_modified
-
-files[] = plugins/export_ui/dfp_tag_ui.class.inc
-files[] = plugins/export_ui/dfp_ctools_export_ui.inc
-files[] = tests/dfp.test
-
-testing_api = 2.x
diff --git a/dfp.info.yml b/dfp.info.yml
new file mode 100755
index 0000000..acfb65b
--- /dev/null
+++ b/dfp.info.yml
@@ -0,0 +1,10 @@
+name: 'Doubleclick for Publishers'
+type: module
+description: 'Integrates DFP Google Publisher Tags onto a site.'
+core: 8.x
+package: Advertising
+configure: dfp.admin_settings
+dependencies:
+  - block
+  - taxonomy
+#  - entity_modified
diff --git a/dfp.install b/dfp.install
index 0a64ccf..8774afb 100644
--- a/dfp.install
+++ b/dfp.install
@@ -5,264 +5,13 @@
  * Installation file for DFP module.
  */
 
-/**
- * Implements hook_enable().
- */
-function dfp_enable() {
-  $t = get_t();
-
-  if (!module_exists('token')) {
-    drupal_set_message($t('While it is not required, the Doubleclick for Publishers administration forms are vastly improved by using the <a href="http://drupal.org/project/token">token module</a>.'));
-  }
-}
-
-/**
- * Implements hook_schema().
- */
-function dfp_schema() {
-  $schema['dfp_tags'] = array(
-    'description' => 'Stores information about DFP ad tags.',
-    'export' => array(
-      'key' => 'machinename',
-      'primary key' => 'adid',
-      'identifier' => 'tag', // Exports will be available as $tag
-      'default hook' => 'default_dfp_tag', // Function hook name.
-      'api' => array(
-        'owner' => 'dfp',
-        'api' => 'default_dfp_tags', // Base name for api include files.
-        'minimum_version' => 1,
-        'current_version' => 1,
-      ),
-      'load callback' => 'dfp_tag_load',
-    ),
-    'fields' => array(
-      'adid' => array(
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'description' => 'Primary ID field for the table. Not used for anything except internal lookups.',
-        'no export' => TRUE, // Do not export database-only keys.
-      ),
-      'machinename' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => "The tag's unique name.",
-      ),
-      'slot' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => "The ad slot's name.",
-      ),
-      'adunit' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => FALSE,
-        'default' => '',
-        'description' => "The ad unit name.",
-      ),
-      'size' => array(
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'description' => "The tag's size(s).",
-      ),
-      'block' => array(
-        'type' => 'int',
-        'default' => 1,
-        'description' => 'If true, a block will be created in which to display this ad tag.',
-      ),
-      'settings' => array(
-        'type' => 'blob',
-        'description' => "Serialized array of settings.",
-        'size' => 'big',
-        'serialize' => TRUE,
-      ),
-    ),
-    'primary key' => array('adid'),
-    'unique keys' => array(
-      'machinename' => array('machinename'),
-    ),
-  );
-
-  return $schema;
-}
-
-/**
- * Implements hook_install().
- */
-function dfp_install() {
-  // Migrate data from the DART module if possible.
-  dfp_update_7000();
-
-  $vocabulary_machinename = 'dfp_ad_categories';
-
-  // If it doesn't already exist, create a vocabulary so that terms can be
-  // targeted in groups.
-  $existing_vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_machinename);
-  if (!$existing_vocabulary) {
-    taxonomy_vocabulary_save((object) array(
-      'name' => 'DFP Ad Categories',
-      'machine_name' => $vocabulary_machinename,
-    ));
-  }
-
-  // Create a new term_reference field for the new vocabulary.
-  if (field_read_field('field_dfp_ad_categories') === FALSE) {
-    $field = array(
-      'field_name' => 'field_dfp_ad_categories',
-      'type' => 'taxonomy_term_reference',
-      'settings' => array(
-        'allowed_values' => array(
-          array(
-            'vocabulary' => $vocabulary_machinename,
-            'parent' => 0
-          ),
-        ),
-      ),
-    );
-    field_create_field($field);
-  }
-}
-
-/**
- * Migrate existing DART tags to DFP tags. Note: this migration will destroy
- * the any DFP global settings that have already been set.
- */
-function dfp_update_7000() {
-  $t = get_t();
-
-  if (module_exists('dart')) {
-    // Migrate global settings.
-    $network_id = variable_get('dart_global_network_id', '');
-    if (!empty($network_id)) {
-      variable_set('dfp_network_id', $network_id);
-    }
-
-    $slug = variable_get('dart_global_slug', '');
-    if (!empty($slug)) {
-      variable_set('dfp_default_slug', $slug);
-    }
-
-    $adunit = array();
-    $adunit[] = variable_get('dart_global_prefix', '');
-    $adunit[] = variable_get('dart_global_site', '');
-    $adunit[] = variable_get('dart_global_zone', '');
-    $adunit = array_filter($adunit);
-    if (!empty($adunit)) {
-      variable_set('dfp_default_adunit', implode('/', $adunit));
-    }
-
-    $js = variable_get('dart_js', '');
-    if (!empty($js)) {
-      variable_set('dfp_injected_js', $js);
-    }
-
-    $asyncronous = variable_get('dart_load_last', NULL);
-    if (!is_null($asyncronous)) {
-      variable_set('dfp_async_rendering', $asyncronous);
-    }
-
-    // Migrate DART tags to DFP tags.
-    if (db_table_exists('dart_tags')) {
-      $dart_tags_query = db_select('dart_tags', 'dt')
-        ->fields('dt')
-        ->execute();
-      while ($tag = $dart_tags_query->fetchObject()) {
-        $tag->settings = unserialize($tag->settings);
-
-        $fields = array(
-          'machinename' => (string) $tag->machinename,
-          'slot' => (string) $tag->pos,
-          'size' => (string) $tag->sz,
-          'block' => (int) $tag->block,
-        );
-
-        $adunit = array();
-        if (isset($tag->settings['overrides']['site'])) {
-          $adunit[] = $tag->settings['overrides']['site'];
-        }
-        if (isset($tag->settings['overrides']['zone'])) {
-          $adunit[] = $tag->settings['overrides']['zone'];
-        }
-        $fields['adunit'] = !empty($adunit) ? implode('/', $adunit) : '';
-
-        $settings = array();
-        if (isset($tag->settings['overrides']['slug'])) {
-          $settings['slug'] = $tag->settings['overrides']['slug'];
-        }
-        if (isset($tag->settings['options']['scriptless'])) {
-          $settings['short_tag'] = $tag->settings['options']['scriptless'];
-        }
-
-        $settings['targeting'] = array();
-        foreach ($tag->settings['key_vals'] as $kv) {
-          $settings['targeting'][] = array('target' => $kv['key'], 'value' => $kv['val']);
-        }
-
-        $fields['settings'] = serialize($settings);
-
-        $insert_tag_query = db_insert('dfp_tags')
-          ->fields($fields)
-          ->execute();
-      }
-    }
-    return $t('All DART tags and settings have been migrated to coresponding DFP tags and settings. It is highly recomended that you uninstall the DART module.');
-  }
-}
-
-/**
- * The network id is now included automatically in the ad unit pattern therefore
- * existing tags need the ad unit id token removed so it doesnt appear twice.
- */
-function dfp_update_7001() {
-  $t = get_t();
-
-  $tags = db_select('dfp_tags', 'd')->fields('d')->execute();
-
-  foreach ($tags as $tag) {
-    if (strpos($tag->adunit, '[dfp_tag:network_id]/') === 0) {
-      $tag->adunit = str_replace('[dfp_tag:network_id]/', '', $tag->adunit);
-      $record = array('adid' => $tag->adid, 'adunit' => $tag->adunit);
-      drupal_write_record('dfp_tags', $record, 'adid');
-    }
-  }
-
-  return $t('The [dfp_tag:network_id] token has been removed from the beginning of the Ad Unit Pattern for all tags because it is now automatically included.');
-}
-
-/**
- * Fix a typo in the name of the DFP Ad Categories vocabulary.
- */
-function dfp_update_7002() {
-  $vocabulary_machinename = 'dfp_ad_categories';
-  $existing_vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_machinename);
-
-  if ($existing_vocabulary && $existing_vocabulary->name == 'DFP Ad Catergories') {
-    $existing_vocabulary->name = 'DFP Ad Categories';
-    taxonomy_vocabulary_save($existing_vocabulary);
-  }
-}
-
-/**
- * Prepend ths NetworkID with a "/" as per Google's recommendations.
- */
-function dfp_update_7003() {
-  $current_network_id = variable_get('dfp_network_id', '');
-  if (!empty($current_network_id) && strpos($current_network_id, "/") !== 0) {
-    variable_set('dfp_network_id', "/" . $current_network_id);
-    drupal_set_message(t('Your network ID has been prepended with a "/" to match Google\'s recommendations. It\'s possible this could effect your reporting on DFP so if you are not certain this change is right for your site, you should remove the "/" by editing the network id on the <a href="admin/structure/dfp_ads/settings">global DFP settings form</a>.'), 'warning');
-  }
-}
-
-/**
- * Add the default setting for slug placement
-*/
-
-function dfp_update_7004() {
-  variable_set('dfp_slug_placement', 0);
-}
+///**
+// * Implements hook_enable().
+// */
+//function dfp_enable() {
+//  $t = get_t();
+//
+//  if (!module_exists('token')) {
+//    drupal_set_message($t('While it is not required, the Doubleclick for Publishers administration forms are vastly improved by using the <a href="http://drupal.org/project/token">token module</a>.'));
+//  }
+//}
diff --git a/dfp.links.action.yml b/dfp.links.action.yml
new file mode 100644
index 0000000..8135f1e
--- /dev/null
+++ b/dfp.links.action.yml
@@ -0,0 +1,5 @@
+entity.dfp_tag.add_form:
+  route_name: entity.dfp_tag.add_form
+  title: 'Add DFP tag'
+  appears_on:
+    - entity.dfp_tag.collection
diff --git a/dfp.links.contextual.yml b/dfp.links.contextual.yml
new file mode 100644
index 0000000..1a47da1
--- /dev/null
+++ b/dfp.links.contextual.yml
@@ -0,0 +1,4 @@
+entity.dfp_tag.edit_form:
+  route_name: entity.dfp_tag.edit_form
+  group: dfp_tag
+  title: Edit DFP tag
diff --git a/dfp.links.menu.yml b/dfp.links.menu.yml
new file mode 100644
index 0000000..a3ddb79
--- /dev/null
+++ b/dfp.links.menu.yml
@@ -0,0 +1,5 @@
+dfp.admin_settings:
+  title: 'DFP Ad Tags'
+  parent: system.admin_structure
+  description: 'Create and manage your DFP ad tags.'
+  route_name: entity.dfp_tag.collection
diff --git a/dfp.links.task.yml b/dfp.links.task.yml
new file mode 100644
index 0000000..d70cc04
--- /dev/null
+++ b/dfp.links.task.yml
@@ -0,0 +1,9 @@
+dfp.admin_settings:
+  title: 'Global settings'
+  route_name: dfp.admin_settings
+  base_route: entity.dfp_tag.collection
+
+entity.dfp_tag.collection:
+  title: List
+  route_name: entity.dfp_tag.collection
+  base_route: entity.dfp_tag.collection
diff --git a/dfp.module b/dfp.module
index b603d41..f00a0a9 100755
--- a/dfp.module
+++ b/dfp.module
@@ -1,15 +1,18 @@
 <?php
 
-define('DFP_GOOGLE_TAG_SERVICES_URL', 'www.googletagservices.com/tag/js/gpt.js');
-define('DFP_GOOGLE_SHORT_TAG_SERVICES_URL', 'pubads.g.doubleclick.net/gampad');
-define('DFP_TOKEN_CACHE', 'dfp:tag_token_results');
+/**
+ * @file
+ * The DFP module.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
  * Implements hook_help().
  */
-function dfp_help($path, $arg) {
-  switch ($path) {
-    case 'admin/help#dfp':
+function dfp_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.dfp':
       $output = '<p>' . t('The Doubleclick For Publishers (DFP) module allows you to integrate Google Publisher Tags onto your site.') . '</p>';
       $output .= '<p>' . t('This module provides you with a general settings form as well as the ability to create a tag (with all its associated data) in the database. You can display your ads as blocks, or add a simple bit of php to your tpl.php file(s) within your theme to indicate where specific tags should be displayed.') . '</p>';
       return $output;
@@ -17,1020 +20,40 @@ function dfp_help($path, $arg) {
 }
 
 /**
- * Implements hook_permission().
- */
-function dfp_permission() {
-  return array(
-    'administer DFP' => array(
-      'title' => t('Administer Doubleclick for Publisher ads'),
-      'description' => t('Users can create, edit, and delete Doubleclick for Publishers (dfp) ad tags and configure how and when they should be displayed.'),
-    ),
-  );
-}
-
-/**
  * Implements hook_theme().
  */
-function dfp_theme($existing, $type, $theme, $path) {
-  $theme_hooks = array(
-    'dfp_tag' => array(
-      'variables' => array(
+function dfp_theme() {
+  return [
+    'dfp_tag' => [
+      'variables' => [
         'tag' => NULL,
-        'slug' => NULL,
-      ),
-      'template' => 'theme/dfp_tag',
-    ),
-    'dfp_short_tag' => array(
-      'variables' => array(
+      ]
+    ],
+    'dfp_short_tag' => [
+      'variables' => [
         'tag' => NULL,
-      ),
-      'template' => 'theme/dfp_short_tag',
-    ),
-    'dfp_target_settings' => array(
-      'render element' => 'form',
-      'file' => 'dfp.admin.inc',
-    ),
-    'dfp_breakpoint_settings' => array(
-      'render element' => 'form',
-      'file' => 'dfp.admin.inc',
-    ),
-    'dfp_adsense_color_settings' => array(
-      'render element' => 'form',
-      'file' => 'dfp.admin.inc',
-    ),
-    'dfp_size_settings' => array(
-      'render element' => 'form',
-      'file' => 'dfp.admin.inc',
-    ),
-  );
-
-  return $theme_hooks;
-}
-
-/**
- * Implements hook_menu().
- */
-function dfp_menu() {
-  $items = array();
-
-  $items['admin/structure/dfp_ads/settings'] = array(
-    'title' => 'Global DFP Settings',
-    'type' => MENU_LOCAL_TASK,
-    'description' => "Configure your site-wide DFP settings.",
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('dfp_admin_settings'),
-    'access arguments' => array('administer DFP'),
-    'file' => 'dfp.admin.inc',
-    'weight' => 5,
-  );
-  $items['admin/structure/dfp_ads/test_page'] = array(
-    'title' => 'DFP Test Page',
-    'type' => MENU_LOCAL_TASK,
-    'description' => "View all your DFP tags on a single page",
-    'page callback' => 'dfp_adtest_page',
-    'access arguments' => array('administer DFP'),
-    'file' => 'dfp.adtest.inc',
-    'weight' => 10,
-  );
-
-  return $items;
-}
-
-/**
- * Implements hook_menu_alter().
- */
-function dfp_menu_alter(&$items) {
-  $items['admin/structure/dfp_ads/list/%ctools_export_ui/edit']['context'] = MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE;
-  $items['admin/structure/dfp_ads/list/%ctools_export_ui/edit']['title'] = t('Edit DFP ad tag');
-}
-
-/**
- * Implements hook_block_info().
- */
-function dfp_block_info() {
-  $tags = dfp_tag_load_all();
-  $blocks = array();
-  $hashes = array();
-
-  foreach ($tags as $tag) {
-    if ($tag->block) {
-      // The block table chokes when the delta is more than 32 characters. To
-      // solve this we create a hash of the machine name when needed.
-      if (drupal_strlen($tag->machinename) >= 32) {
-        $delta = md5($tag->machinename);
-        $hashes[$delta] = $tag->machinename;
-      }
-      else {
-        $delta = $tag->machinename;
-      }
-
-      $blocks[$delta]['info'] = t('DFP tag: @slotname', array('@slotname' => $tag->slot));
-      $blocks[$delta]['cache'] = DRUPAL_CACHE_PER_PAGE;
-    }
-  }
-
-  // Only save hashes if they have changed.
-  $old_hashes = variable_get('dfp_block_hashes', array());
-  if ($hashes != $old_hashes) {
-    variable_set('dfp_block_hashes', $hashes);
-  }
-
-  return $blocks;
-}
-
-/**
- * Implements hook_block_view().
- */
-function dfp_block_view($delta) {
-  $block = array();
-
-  // If this is 32, this should be an md5 hash.
-  if (drupal_strlen($delta) == 32) {
-    $hashes = variable_get('dfp_block_hashes', array());
-    if (!empty($hashes[$delta])) {
-      $delta = $hashes[$delta];
-    }
-  }
-
-  $tag = dfp_tag_load($delta);
-
-  if (empty($tag->disabled)) {
-    $block['content'] = dfp_tag($delta);
-    $block['content']['#contextual_links'] = array(
-      'dfp' => array('admin/structure/dfp_ads/list', array($delta, 'edit'))
-    );
-  }
-
-  return $block;
-}
-
-/**
- * Implements hook_entity_view().
- */
-function dfp_entity_view($entity, $type, $view_mode, $langcode) {
-  $dfp_targeting_terms = &drupal_static('dfp_entity_targeting_terms', array());
-
-  if (variable_get('dfp_enable_ad_categories', 0) && $view_mode == 'full') {
-    // If this entity is itself a taxonomy term add it to the
-    // dfp_targetting_terms array. Check it to see if a DFP Ad Category
-    // has been assigned to it. If so, add that term to the array instead. Note
-    // that we check all types of entities here because any fieldable entity can
-    // have a taxonomy term reference field attached to it.
-    if ($type == 'taxonomy_term') {
-      $dfp_targeting_terms[] = _dfp_get_ad_category($entity, TRUE);
-    }
-
-    // Find all taxonomy terms attached to the given entity and add them to the
-    // dfp_targeting_terms array. Check each term to see if a DFP Ad Category
-    // has been assigned to it. If so, add that term to the array instead.
-    foreach (element_children($entity->content) as $key) {
-      if (isset($entity->content[$key]['#field_type']) && $entity->content[$key]['#field_type'] == 'taxonomy_term_reference') {
-        $terms = field_view_field($type, $entity, $key);
-        if (isset($terms['#items']) && is_array($terms['#items'])) {
-          foreach ($terms['#items'] as $item) {
-            if (array_key_exists('taxonomy_term', $item)) {
-              $dfp_targeting_terms[] = _dfp_get_ad_category($item['taxonomy_term'], TRUE);
-            }
-          }
-        }
-      }
-    }
-
-    $dfp_targeting_terms = array_unique($dfp_targeting_terms);
-  }
-}
-
-/**
- * Implements hook_ctools_plugin_directory().
- */
-function dfp_ctools_plugin_directory($module, $type) {
-  // Load the export_ui plugin.
-  if ($type == 'export_ui' || $type == 'content_types') {
-    return 'plugins/' . $type;
-  }
-}
-
-/**
- * Implements hook_context_registry().
- */
-function dfp_context_registry() {
-  return array(
-    'reactions' => array(
-      'dfp_tags' => array(
-        'title' => t('DFP Tags'),
-        'plugin' => 'dfp_context_reaction_tags',
-      ),
-      'dfp_outofpage' => array(
-        'title' => t('DFP Out of page'),
-        'plugin' => 'dfp_context_reaction_outofpage',
-      ),
-      'dfp_settings' => array(
-        'title' => t('DFP Variables'),
-        'plugin' => 'dfp_context_reaction_settings',
-      ),
-      'dfp_adunit' => array(
-        'title' => t('DFP AdUnit'),
-        'plugin' => 'dfp_context_reaction_adunit',
-      ),
-      'dfp_adsize' => array(
-        'title' => t('DFP Sizes'),
-        'plugin' => 'dfp_context_reaction_sizes',
-      ),
-    ),
-  );
-}
-
-/**
- * Implements hook_context_plugins().
- */
-function dfp_context_plugins() {
-  $plugins = array();
-  $plugins['dfp_context_reaction_tags'] = array(
-    'handler' => array(
-      'path' => drupal_get_path('module', 'dfp') . '/plugins/contexts',
-      'file' => 'dfp_context_reaction_tags.inc',
-      'class' => 'dfp_context_reaction_tags',
-      'parent' => 'context_reaction',
-    ),
-  );
-  $plugins['dfp_context_reaction_outofpage'] = array(
-    'handler' => array(
-      'path' => drupal_get_path('module', 'dfp') . '/plugins/contexts',
-      'file' => 'dfp_context_reaction_outofpage.inc',
-      'class' => 'dfp_context_reaction_outofpage',
-      'parent' => 'context_reaction',
-    ),
-  );
-  $plugins['dfp_context_reaction_settings'] = array(
-    'handler' => array(
-      'path' => drupal_get_path('module', 'dfp') . '/plugins/contexts',
-      'file' => 'dfp_context_reaction_settings.inc',
-      'class' => 'dfp_context_reaction_settings',
-      'parent' => 'context_reaction',
-    ),
-  );
-  $plugins['dfp_context_reaction_adunit'] = array(
-    'handler' => array(
-      'path' => drupal_get_path('module', 'dfp') . '/plugins/contexts',
-      'file' => 'dfp_context_reaction_adunit.inc',
-      'class' => 'dfp_context_reaction_adunit',
-      'parent' => 'context_reaction',
-    ),
-  );
-  $plugins['dfp_context_reaction_sizes'] = array(
-    'handler' => array(
-      'path' => drupal_get_path('module', 'dfp') . '/plugins/contexts',
-      'file' => 'dfp_context_reaction_sizes.inc',
-      'class' => 'dfp_context_reaction_sizes',
-      'parent' => 'context_reaction',
-    ),
-  );
-  return $plugins;
-}
-
-/**
- * Implements hook_token_info().
- */
-function dfp_token_info() {
-  $type = array(
-    'name' => t('DFP Ad Tags'),
-    'description' => t('Tokens related to a given DFP ad tag.'),
-    'needs-data' => 'tag',
-  );
-
-  $tag['slot'] = array(
-    'name' => t('Slot Name'),
-    'description' => t("The name of the ad slot defined by this tag."),
-  );
-  $tag['network_id'] = array(
-    'name' => t("Network ID"),
-    'description' => t("The unique ID provided by Google."),
-  );
-  $tag['ad_categories'] = array(
-    'name' => t("DFP Ad Categories"),
-    'description' => t("The DFP Ad Categories or uncategorized taxonomy terms attached to the entities currently being displayed to the user."),
-  );
-  $tag['url_parts'] = array(
-    'name' => t("URL Parts (n)"),
-    'description' => t('**Deprecated. See <a href="http://drupal.org/node/1812372" target="_blank">this issue<a> for alternitives.'),
-  );
-
-  return array(
-    'types' => array('dfp_tag' => $type),
-    'tokens' => array('dfp_tag' => $tag),
-  );
-}
-
-/**
- * Implements hook_tokens().
- */
-function dfp_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $replacements = array();
-
-  if ($type == 'dfp_tag') {
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'slot':
-          if (!empty($data['tag'])) {
-            $replacements[$original] = check_plain($data['tag']->slot);
-          }
-          break;
-
-        case 'network_id':
-          $replacements[$original] = check_plain(variable_get('dfp_network_id', ''));
-          break;
-
-        case 'ad_categories':
-          $term_names = &drupal_static('dfp_entity_targeting_terms', array());
-          $replacements[$original] = implode(',', $term_names);
-          break;
-      }
-    }
-
-    if ($created_tokens = token_find_with_prefix($tokens, 'url_parts')) {
-      foreach ($created_tokens as $name => $original) {
-        $url_parts = explode('/', $_GET['q']);
-        $replacements[$original] = implode('/', array_slice($url_parts, 0, $name));
-      }
-    }
-  }
-
-  return $replacements;
-}
-
-/**
- * Implements preprocess_html().
- */
-function dfp_preprocess_html($variables) {
-  // Add the header js here so that enough information has been loaded for
-  // tokens to work properly.
-  _dfp_js_global_settings();
-}
-
-
-/**
- * Return a render array for the tag specified by machinename.
- */
-function dfp_tag($machinename) {
-  $tag = dfp_tag_load($machinename);
-  $render_array = array();
-
-  if (!$tag) {
-    watchdog('dfp', 'Unknown ad tag %machinename passed to dfp_tag().', array('%machinename' => $machinename), WATCHDOG_WARNING);
-  }
-  else {
-    $tag->slug = dfp_format_slug($tag->slug);
-    $slug_placement = variable_get('dfp_slug_placement', 0);
-
-    if (!empty($tag)) {
-      $render_array = array(
-        'dfp_wrapper' => array(
-          '#type' => 'container',
-          '#attributes' => array(
-            'id' => $tag->wrapper_id,
-            'class' => array(
-              'dfp-tag-wrapper',
-            ),
-          ),
-          'tag' => array(
-            '#theme' => $tag->short_tag ? 'dfp_short_tag' : 'dfp_tag',
-            '#tag' => $tag,
-          ),
-        ),
-      );
-      if (!empty($tag->slug) && $slug_placement == 0) {
-        $render_array['dfp_wrapper']['slug_wrapper'] = array(
-          '#type' => 'container',
-          '#attributes' => array(
-            'class' => array(
-              'slug',
-            ),
-          ),
-          'slug' => array(
-            '#markup' => $tag->slug,
-          ),
-          '#weight' => -1,
-        );
-      }
-    }
-  }
-
-  return $render_array;
-}
-
-/**
- * Load function.
- *
- * @param string $machinename
- *
- * @return object
- *
- */
-function dfp_tag_load($machinename) {
-  ctools_include('export');
-
-  $tags = &drupal_static(__FUNCTION__, array());
-
-  if (!isset($tags[$machinename])) {
-    // Load the tag.
-    $result = ctools_export_load_object('dfp_tags', 'names', array($machinename));
-    if (isset($result[$machinename])) {
-      $tag = $result[$machinename];
-    }
-    else {
-      return NULL;
-    }
-
-    // Store the original tag. This is used by the tag edit form.
-    $tag->raw = clone $tag;
-
-    // Allow modules to alter the raw tag object.
-    drupal_alter('dfp_tag_load', $tag);
-
-    // Move the settings out of the settings array.
-    foreach ($tag->settings as $key => $val) {
-      $tag->{$key} = $val;
-    }
-
-    // Configure this tag based on the defined settings.
-    $tag->wrapper_id = 'dfp-ad-' . $tag->machinename . '-wrapper';
-    $tag->placeholder_id = 'dfp-ad-' . $tag->machinename;
-
-    // Allow modules to alter the fully-loaded tag object.
-    drupal_alter('dfp_tag', $tag);
-
-    // Statically cache the fully loaded tag.
-    $tags[$machinename] = $tag;
-  }
-  else {
-    // Use the statically cached tag object.
-    $tag = $tags[$machinename];
-  }
-
-  return $tag;
-}
-
-/**
- * Load all dfp ad tags.
- *
- * @param boolean $include_disabled
- *
- * @return array of tags.
- */
-function dfp_tag_load_all($include_disabled = FALSE) {
-  ctools_include('export');
-
-  $tags = ctools_export_crud_load_all('dfp_tags');
-  foreach ($tags as $key => $tag) {
-    if (!$include_disabled && isset($tag->disabled) && $tag->disabled) {
-      unset($tags[$key]);
-    }
-  }
-
-  return $tags;
-}
-
-/**
- * Save a single tag.
- */
-function dfp_tag_save(&$tag) {
-  $update = (isset($tag->adid) && is_numeric($tag->adid)) ? array('adid') : array();
-  return drupal_write_record('dfp_tags', $tag, $update);
-}
-
-/**
- * Alter a dfp tag object to integrate with the contexts module.
- */
-function dfp_dfp_tag_load_alter(&$tag) {
-  // Execute context reactions for each plugin.
-  if (module_exists('context')) {
-    $contexts = dfp_context_registry();
-    foreach ($contexts['reactions'] as $key => $val) {
-      if ($plugin = context_get_plugin('reaction', $key)) {
-        $plugin->execute($tag);
-      }
-    }
-  }
-
-  // Handle ad testing.
-  module_load_include('inc', 'dfp', 'dfp.adtest');
-  dfp_adtest_alter_tag($tag);
-}
-
-/**
- * Alter the vertical tabs group in which the exportable scheduler form should
- * live.
- */
-function dfp_exportable_scheduler_form_group_alter(&$group) {
-  $group = 'settings';
-}
-
-/**
- * Form alter for the ctools_export_ui_edit_item_form.
- */
-function dfp_form_ctools_export_ui_edit_item_form_alter(&$form, &$form_state) {
-  if (arg(2) == 'context') {
-    // Make sure that dfp.admin.inc is included on the context ui form to avoid
-    // errors when doing an ajax submit.
-    form_load_include($form_state, 'inc', 'dfp', 'dfp.admin');
-    //array_unshift($form['#submit'], 'dps_targeting_form_submit');
-  }
-}
-
-/**
- * Format the given array of values to be displayed as part of a javascript.
- *
- * @param array $variables
- *  'values' => A simple array of targeting values. Ex. val1, val2, val3
- *
- * @return string
- *   If $values had one item, then a string in the format ["val1"]. If $values
- *   has more than one item, then a string in the format ["val1","val2","val3"].
- *
- */
-function dfp_format_targeting($targeting, $tag = '') {
-  foreach ($targeting as $key => &$target) {
-    $target['target'] = '"' . check_plain($target['target']) . '"';
-    $target['value'] = dfp_token_replace(check_plain($target['value']), $tag, array('sanitize' => TRUE, 'clear' => TRUE));
-
-    // The target value could be blank if tokens are used. If so, removed it.
-    if (empty($target['value'])) {
-      unset($targeting[$key]);
-      continue;
-    }
-
-    // Allow other modules to alter the target.
-    drupal_alter('dfp_target', $target);
-
-    // Convert the values into an array and trim the whitespace from each value.
-    $values = explode(',', $target['value']);
-    $values = array_map('trim', $values);
-
-    if (count($values) == 1) {
-      $target['value'] = '"' . $values[0] . '"';
-    }
-    elseif (count($values) > 1) {
-      $target['value'] = '["' . implode('","', $values) . '"]';
-    }
-  }
-
-  return $targeting;
-}
-
-/**
- * Format the the size of an ad tag.
- *
- * @param array $variables
- *   'size' => A string in the format 123x321,987x789.
- *
- * @return string
- *   A string in the format [456, 654] or [[123, 321], [987, 789]].
- *
- */
-function dfp_format_size($size) {
-  $formatted_sizes = array();
-
-  $sizes = explode(',', check_plain($size));
-  foreach ($sizes as $size) {
-    $formatted_size = explode('x', trim($size));
-    $formatted_sizes[] = '[' . implode(', ', $formatted_size) . ']';
-  }
-
-  return count($formatted_sizes) == 1 ? $formatted_sizes[0] : '[' . implode(', ', $formatted_sizes) . ']';
-}
-
-/**
- * Format the the size of an ad tag.
- *
- * @param array $variables
- *   'slug' => A label for for this particular ad tag.
- *
- * @return string
- *   If $slug is none, an empty string will be returned; if $slug is a non-empty
- *   string then it will be returned unchanged; if $slug is empty, then the
- *   default slug will be returned.
- *
- */
-function dfp_format_slug($slug) {
-  $formatted_slug = t(check_plain(variable_get('dfp_default_slug', '')));
-
-  if ($slug == '<none>') {
-    $formatted_slug = "";
-  }
-  elseif (!empty($slug)) {
-    $formatted_slug = $slug;
-  }
-
-  return $formatted_slug;
-}
-
-/**
- * Return the term object to use as the DFP Ad Category given a specific term.
- *
- * @param object $term
- *   The term object to analyze. If it is tagged with a DFP Ad Cateegory, then
- *   that term is returned, otherwise the original term is returned unchanged.
- *
- * @param boolean $clean_string
- *   If true, use ctools_cleanstring. In future versions, this should default to
- *   TRUE, but for now it defaults to FALSE.
- *
- * @return string
- *   The term name to be included in an ad tag.
- */
-function _dfp_get_ad_category($term, $clean_string = FALSE) {
-  if (!empty($term->field_dfp_ad_categories)) {
-    $term = taxonomy_term_load($term->field_dfp_ad_categories[LANGUAGE_NONE][0]['tid']);
-  }
-
-  $term_name = $term->name;
-  if ($clean_string) {
-    ctools_include('cleanstring');
-    $term_name = ctools_cleanstring($term_name, array(
-      'lower_case' => TRUE,
-    ));
-  }
-
-  return $term->name;
-}
-
-/**
- * Helper function to include javascript variables, etc in the header above all
- * slot definitions.
- */
-function _dfp_js_global_settings() {
-  // Initialize the google varibales and inject user-defined javascript.
-  $js = 'var googletag = googletag || {};' . "\n";
-  $js .= 'googletag.cmd = googletag.cmd || [];';
-  $js .= variable_get('dfp_injected_js', '') . "\n";
-
-  // Add a place to store the slot name variable
-  $js .= 'googletag.slots = googletag.slots || {};';
-
-  $options = array(
-    'type' => 'inline',
-    'group' => JS_LIBRARY,
-    'every_page' => TRUE,
-    'weight' => -10,
-    'force header' => TRUE,
-  );
-  drupal_add_js($js, $options);
-
-  // Include a script tag for the Google Tag Services.
-  // @todo: One day this should include an async=true attribute. See #1140356.
-  //        However, there is a good chance this might break <= IE8.
-  $options['type'] = 'external';
-  $options['weight']++;
-  drupal_add_js(($GLOBALS['is_https'] ? "https://" : "http://") . DFP_GOOGLE_TAG_SERVICES_URL, $options);
-
-  // Add global settings with a heavy weight so that they appear after all the
-  // defineSlot() calls otherwise IE8 and Opera fail to display ads properly.
-  $js = 'googletag.cmd.push(function() {' . "\n";
-  if (variable_get('dfp_async_rendering', 1)) {
-    $js .= '  googletag.pubads().enableAsyncRendering();' . "\n";
-  }
-  else {
-     $js .= '  googletag.pubads().enableSyncRendering();' . "\n";
-  }
-  if (variable_get('dfp_single_request', 1)) {
-    $js .= '  googletag.pubads().enableSingleRequest();' . "\n";
-  }
-  switch (variable_get('dfp_collapse_empty_divs', 1)) {
-    case 1:
-      $js .= '  googletag.pubads().collapseEmptyDivs();' . "\n";
-      break;
-    case 2:
-      $js .= '  googletag.pubads().collapseEmptyDivs(true);' . "\n";
-      break;
-  }
-  if (variable_get('dfp_disable_init_load', 0)) {
-    $js .= '  googletag.pubads().disableInitialLoad();' . "\n";
-  }
-
-  // Set global targeting values for this page.
-  $targeting = variable_get('dfp_targeting', array());
-  drupal_alter('dfp_global_targeting', $targeting);
-  $targeting = dfp_format_targeting($targeting);
-  foreach ($targeting as $key => $target) {
-    if (!empty($target['target']) && !empty($target['value'])) {
-      $js .= '  googletag.pubads().setTargeting(' . $target['target'] . ', ' . $target['value'] . ');' . "\n";
-    }
-  }
-
-  $js .= '});' . "\n";
-  $js .= variable_get('dfp_injected_js2', '') . "\n";
-  $js .= 'googletag.enableServices();';
-
-  $options = array(
-    'type' => 'inline',
-    'group' => JS_DEFAULT,
-    'every_page' => TRUE,
-    'weight' => 10,
-    'force header' => TRUE,
-  );
-  drupal_add_js($js, $options);
-}
-
-/**
- * Helper function to build the javascript needed to define an ad slot and add
- * it to the head tag.
- */
-function _dfp_js_slot_definition($tag) {
-  // Avoid accidentaly adding ad slot definition more than once.
-  if (isset($tag->processed) && $tag->processed === TRUE) {
-    watchdog('dfp', 'DFP tag %machinename is being added to the page more than once.', array('%machinename' => $tag->machinename), WATCHDOG_WARNING);
-    return;
-  }
-
-  // Add the js needed to define this adSlot to <head>.
-  $js = '';
-  // Start by defining breakpoints for this ad.
-  if (!empty($tag->breakpoints)) {
-    $breakpoints = $tag->breakpoints;
-    $js .= 'var mapping = googletag.sizeMapping()' . "\n";
-    foreach ($breakpoints as $breakpoint) {
-      $js .= '  .addSize(' . dfp_format_size($breakpoint['browser_size']) . ', ' . dfp_format_size($breakpoint['ad_sizes']) . ')' . "\n";
-    }
-    $js .= '  .build();' . "\n";
-  }
-  if (!empty($tag->settings['out_of_page'])) {
-    $js .= 'googletag.slots["' . $tag->machinename . '"] = googletag.defineOutOfPageSlot("' . $tag->adunit . '", "' . $tag->placeholder_id . '")' . "\n";
-  }
-  else {
-    $js .= 'googletag.slots["' . $tag->machinename . '"] = googletag.defineSlot("' . $tag->adunit . '", ' . $tag->size . ', "' . $tag->placeholder_id . '")' . "\n";
-  }
-  $click_url = variable_get('dfp_click_url', '');
-  if (!empty($click_url)) {
-    if (!preg_match("/^https?:\/\//", $click_url)) {
-      // relative URL; prepend site domain
-      $click_url = (($GLOBALS['is_https']) ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . "/" . $click_url;
-    }
-    $js .= '  .setClickUrl("' . $click_url . '")' . "\n";
-  }
-  $js .= '  .addService(googletag.pubads())' . "\n";
-
-  if (!empty($tag->adsense_ad_types)) {
-    $js .= '  .set("adsense_ad_types", "' . $tag->adsense_ad_types . '")' . "\n";
-  }
-  if (!empty($tag->adsense_channel_ids)) {
-    $js .= '  .set("adsense_channel_ids", "' . $tag->adsense_channel_ids . '")' . "\n";
-  }
-  foreach ($tag->adsense_colors as $key => $val) {
-    if (!empty($val)) {
-      $key = 'adsense_' . $key . '_color';
-      $val = '#' . drupal_strtoupper($val);
-      $js .= '  .set("' . $key . '", "' . $val . '")' . "\n";
-    }
-  }
-  $targeting = dfp_format_targeting($tag->targeting, $tag);
-  foreach ($targeting as $target) {
-    $js .= '  .setTargeting(' . $target['target'] . ', ' . $target['value'] . ')' . "\n";
-  }
-  // Apply size mapping when there are breakpoints.
-  if (!empty($tag->breakpoints)) {
-    $js .= '  .defineSizeMapping(mapping)' . "\n";
-  }
-  $js = rtrim($js, "\n") . ';';
-
-  $options = array(
-    'type' => 'inline',
-    'group' => JS_LIBRARY,
-    'weight' => 0,
-    'force header' => TRUE,
-  );
-  drupal_add_js($js, $options);
-}
-
-/**
- * Prepare token replacement values.
- *
- * @param  (optional) object $tag
- * @return array
- */
-function _dfp_prepare_tokens($tag = NULL) {
-  global $user;
-
-  $data = array();
-  $data['user'] = $user;
-  $data['node'] = menu_get_object();
-
-  // We cannot use menu_get_object because the views version of the taxonomy
-  // term pages will not work properly.
-  $data['term'] = (arg(0) == 'taxonomy' && arg(1) == 'term' && is_numeric(arg(2))) ? taxonomy_term_load(arg(2)) : NULL;
-
-  if (!empty($tag)) {
-    $data['tag'] = $tag;
-  }
-
-  return $data;
-}
-
-/**
- * Check adunit has a value and/or if the global should be used.
- *
- * @param  object $tag
- * @return object $tag
- */
-function _dfp_prepare_adunit($tag) {
-  $global_adunit = variable_get('dfp_default_adunit', '');
-  if (empty($tag->adunit) && !empty($global_adunit)) {
-    $tag->adunit = $global_adunit;
-  }
-
-  return $tag;
-}
-
-/**
- * Replaces all tokens in a given string with appropriate values, with caching.
- *
- * This function is a memoizing wrapper for token_replace(), which is quite slow
- * and inefficient. It takes advantage of specific knowledge about how DFP works
- * to cache the result of token replacement. It is not a fully general solution
- * but works for this module.
- *
- * @param string $text
- *   A string potentially containing replaceable tokens.
- * @param stdClass $tag
- *   The tag for which we are encoding a string. This value will be provided
- *   as additional context to token_replace().
- * @param array $options
- *   An array of options to pass to token_replace().  See that function for
- *   further documentation.
- * @return string
- *   Text with tokens replaced.
- *
- * @see token_replace()
- */
-function dfp_token_replace($text, $tag = NULL, array $options = array()) {
-  $processed_strings =& drupal_static(__FUNCTION__, NULL);
-
-  // Short-circuit the degenerate case, just like token_replace() does.
-  $text_tokens = token_scan($text);
-  if (empty($text_tokens)) {
-    return $text;
-  }
-
-  // Get the possible replacement sources.
-  // @todo This doesn't vary, so we could probably refactor it to be cached,
-  // too.
-  $data = _dfp_prepare_tokens($tag);
-
-  // Determine the cache key for this text string. That way we can cache
-  // reliably.
-  $key = _dfp_token_replace_make_key($text, $data);
-
-  // Use the same granularity as the blocks.
-  $cache_item = DFP_TOKEN_CACHE . ':' . implode(':', drupal_render_cid_parts(DRUPAL_CACHE_PER_PAGE));
-
-  // Lookup any already-cached token replacements.
-  if (is_null($processed_strings)) {
-    $cache = cache_get($cache_item, 'cache');
-    $processed_strings = $cache
-      ? $cache->data
-      : array();
-  }
-
-  // If the processed string we're looking for isn't already in the cache,
-  // then, and only then, do we call the expensive token_replace() (and cache
-  // the result).
-  if (!isset($processed_strings[$key]) || is_null($processed_strings[$key])) {
-    // Regenerate this particular replacement.
-    $processed_strings[$key] = token_replace($text, $data, $options);
-    $lifetime = variable_get('dfp_token_cache_lifetime', 0);
-    $expire_at = ($lifetime == 0) ? CACHE_TEMPORARY : (REQUEST_TIME + $lifetime);
-    cache_set($cache_item, $processed_strings, 'cache', $expire_at);
-  }
-
-  return $processed_strings[$key];
-}
-
-/**
- * Generates an identifying key for the lookup to be processed.
- *
- * @param string $text
- *   The text to be processed.
- * @param array $data
- *   The array of data parameters that will be passed to token_generate().
- *   We'll use knowledge of what is expected in that array to build a
- *   meaningful lookup key.
- * @return string
- *   The key in the lookup array that corresponds to this tokenization request.
- */
-function _dfp_token_replace_make_key($text, array $data) {
-  // $text may be arbitrarily long, which can slow-down lookups. Hashing it
-  // keeps uniqueness but guarantees a manageable size. Since this value won't
-  // be used as the cache key itself we're not limited to 255 characters but
-  // it will be nicer on array lookups in PHP.
-  $keys[] = sha1($text);
-  $keys[] = isset($data['node']->nid) ? $data['node']->nid . '-' . entity_modified_last('node', $data['node'])  : NULL;
-  $keys[] = isset($data['user']->uid) ? $data['user']->uid . '-' . entity_modified_last('user', $data['user']) : NULL;
-  $keys[] = isset($data['term']->tid) ? $data['term']->tid . '-' . entity_modified_last('taxonomy_term', $data['term']) : NULL;
-  $keys[] = isset($data['tag']->machinename) ? $data['tag']->machinename . '-' . entity_modified_last('tag', $data['tag']) : NULL;
-
-  return implode('|', array_filter($keys));
-}
-
-/**
- * Preprocess function for DFP tags.
- */
-function template_preprocess_dfp_tag(&$variables) {
-  $tag = $variables['tag'];
-  $tag = _dfp_prepare_adunit($tag);
-  $slug_placement = variable_get('dfp_slug_placement', 0);
-
-  // Format certain tag properties for display.
-  $tag->adunit = dfp_token_replace('[dfp_tag:network_id]/' . $tag->adunit, $tag, array('sanitize' => TRUE, 'clear' => TRUE));
-  $tag->size = dfp_format_size($tag->size);
-  $tag->slug = dfp_format_slug($tag->slug);
-
-  // Create the attributes for the wrapper div and placeholder div.
-  $variables['placeholder_attributes'] = array(
-    'id' => $tag->placeholder_id,
-    'class' => array(
-      'dfp-tag-wrapper',
-    ),
-  );
-
-  if (!empty($tag->slug) && $slug_placement == 1) {
-    $variables['slug'] = array(
-      '#type' => 'container',
-      '#attributes' => array(
-        'class' => array(
-          'slug',
-        ),
-      ),
-      'slug' => array(
-        '#markup' => $tag->slug,
-      ),
-      '#weight' => -1,
-    );
-  }
-
-  // Define a javascript ad slot for this tag.
-  _dfp_js_slot_definition($tag);
-
-  // Flag the slot as processed.
-  $variables['tag']->processed = TRUE;
-}
-
-/**
- * Preprocess function for DFP tags.
- */
-function template_preprocess_dfp_short_tag(&$variables) {
-  static $tile = 0;
-  $tag = $variables['tag'];
-  $tag = _dfp_prepare_adunit($tag);
-
-  // Build a key|vals array and allow third party modules to modify it.
-  $keyvals = array();
-  $keyvals['iu'] = dfp_token_replace('/[dfp_tag:network_id]/' . $tag->adunit, $tag, array('sanitize' => TRUE, 'clear' => TRUE));
-  $keyvals['sz'] = str_replace(',', '|', check_plain($tag->raw->size));
-  $keyvals['c'] = rand(10000, 99999);
-
-  $targets = array();
-  foreach ($tag->targeting as $data) {
-    $targets[] = check_plain($data['target']) . '=' . check_plain($data['value']);
-  }
-  if (!empty($targets)) {
-    $keyvals['t'] = implode('&', $targets);
-  }
-  drupal_alter('dfp_short_tag_keyvals', $keyvals);
-
-  $variables['url_jump'] = 'http://' . DFP_GOOGLE_SHORT_TAG_SERVICES_URL . '/jump?' . drupal_http_build_query($keyvals);
-  $variables['url_ad'] = 'http://' . DFP_GOOGLE_SHORT_TAG_SERVICES_URL . '/ad?' . drupal_http_build_query($keyvals);
-}
-
-/**
- * Implements of hook_page_build().
- */
-function dfp_page_build(&$page) {
-  $html = '';
-
-  $tags = dfp_tag_load_all();
-  foreach ($tags as $tag) {
-    if (isset($tag->settings['out_of_page']) && $tag->settings['out_of_page'] == TRUE) {
-      drupal_alter('dfp_tag_load', $tag);
-    }
-  }
-
-  if (module_exists('context')) {
-    if ($plugin = context_get_plugin('reaction', 'dfp_outofpage')) {
-      if (isset($plugin->out_of_page_tags)) {
-        foreach ($plugin->out_of_page_tags as $key => $machinename) {
-          $tag = dfp_tag_load($machinename);
-          if (empty($tag->disabled)) {
-            $dfp_tag = dfp_tag($machinename);
-            $dfp_tag['dfp_wrapper']['#attributes']['class'][] = 'element-hidden';
-            $html .= render($dfp_tag);
-          }
-        }
-      }
-    }
-  }
-
-  // Add Out Of Page slots on the top of the page.
-  $page['page_top']['dfp_out_of_page'] = array(
-    '#weight' => -1000,
-    '#markup' => $html,
-  );
+        'url_jump' => NULL,
+        'url_ad' => NULL,
+      ],
+    ],
+    'dfp_js_head_top' => [
+      'variables' => [
+        'google_tag_services_url' => NULL,
+      ],
+    ],
+    'dfp_js_head_bottom' => [
+      'variables' => [
+        'async_rendering' => NULL,
+        'single_request' => NULL,
+        'collapse_empty_divs' => NULL,
+        'disable_init_load' => NULL,
+        'targeting' => [],
+      ],
+    ],
+    'dfp_slot_definition_js' => [
+      'variables' => [
+        'tag' => NULL,
+      ]
+    ],
+  ];
 }
diff --git a/dfp.old.module b/dfp.old.module
new file mode 100755
index 0000000..38af17e
--- /dev/null
+++ b/dfp.old.module
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * Implements hook_entity_view().
+ */
+function dfp_entity_view($entity, $type, $view_mode, $langcode) {
+  $dfp_targeting_terms = &drupal_static('dfp_entity_targeting_terms', array());
+
+  if (variable_get('dfp_enable_ad_categories', 0) && $view_mode == 'full') {
+    // If this entity is itself a taxonomy term add it to the
+    // dfp_targetting_terms array. Check it to see if a DFP Ad Category
+    // has been assigned to it. If so, add that term to the array instead. Note
+    // that we check all types of entities here because any fieldable entity can
+    // have a taxonomy term reference field attached to it.
+    if ($type == 'taxonomy_term') {
+      $dfp_targeting_terms[] = _dfp_get_ad_category($entity, TRUE);
+    }
+
+    // Find all taxonomy terms attached to the given entity and add them to the
+    // dfp_targeting_terms array. Check each term to see if a DFP Ad Category
+    // has been assigned to it. If so, add that term to the array instead.
+    foreach (element_children($entity->content) as $key) {
+      if (isset($entity->content[$key]['#field_type']) && $entity->content[$key]['#field_type'] == 'taxonomy_term_reference') {
+        $terms = field_view_field($type, $entity, $key);
+        if (isset($terms['#items']) && is_array($terms['#items'])) {
+          foreach ($terms['#items'] as $item) {
+            if (array_key_exists('taxonomy_term', $item)) {
+              $dfp_targeting_terms[] = _dfp_get_ad_category($item['taxonomy_term'], TRUE);
+            }
+          }
+        }
+      }
+    }
+
+    $dfp_targeting_terms = array_unique($dfp_targeting_terms);
+  }
+}
+
+/**
+ * Return the term object to use as the DFP Ad Category given a specific term.
+ *
+ * @param object $term
+ *   The term object to analyze. If it is tagged with a DFP Ad Cateegory, then
+ *   that term is returned, otherwise the original term is returned unchanged.
+ *
+ * @param boolean $clean_string
+ *   If true, use ctools_cleanstring. In future versions, this should default to
+ *   TRUE, but for now it defaults to FALSE.
+ *
+ * @return string
+ *   The term name to be included in an ad tag.
+ */
+function _dfp_get_ad_category($term, $clean_string = FALSE) {
+  if (!empty($term->field_dfp_ad_categories)) {
+    $term = taxonomy_term_load($term->field_dfp_ad_categories[LANGUAGE_NONE][0]['tid']);
+  }
+
+  $term_name = $term->name;
+  if ($clean_string) {
+    ctools_include('cleanstring');
+    $term_name = ctools_cleanstring($term_name, array(
+      'lower_case' => TRUE,
+    ));
+  }
+
+  return $term->name;
+}
diff --git a/dfp.permissions.yml b/dfp.permissions.yml
new file mode 100644
index 0000000..f43768a
--- /dev/null
+++ b/dfp.permissions.yml
@@ -0,0 +1,3 @@
+administer DFP:
+  title: 'Administer Doubleclick for Publisher ads'
+  description: 'Users can create, edit, and delete Doubleclick for Publishers (dfp) ad tags and configure how and when they should be displayed.'
diff --git a/dfp.routing.yml b/dfp.routing.yml
new file mode 100644
index 0000000..a378d7d
--- /dev/null
+++ b/dfp.routing.yml
@@ -0,0 +1,53 @@
+dfp.admin_settings:
+  path: 'admin/structure/dfp/settings'
+  defaults:
+    _form: '\Drupal\dfp\Form\AdminSettings'
+    _title: 'Global DFP Settings'
+  requirements:
+    _permission: 'administer DFP'
+
+dfp.test_page:
+  path: 'admin/structure/dfp/test_page'
+  requirements:
+    _permission: 'administer DFP'
+#  $items['admin/structure/dfp_ads/test_page'] = array(
+#    'title' => 'DFP Test Page',
+#    'type' => MENU_LOCAL_TASK,
+#    'description' => "View all your DFP tags on a single page",
+#    'page callback' => 'dfp_adtest_page',
+#    'access arguments' => array('administer DFP'),
+#    'file' => 'dfp.adtest.inc',
+#    'weight' => 10,
+#  );
+
+entity.dfp_tag.add_form:
+  path: '/admin/structure/dfp/tags/add'
+  defaults:
+    _entity_form: 'dfp_tag.add'
+    _title: 'Add DFP tag'
+  requirements:
+    _permission: 'administer DFP'
+
+entity.dfp_tag.edit_form:
+  path: '/admin/structure/dfp/tags/manage/{dfp_tag}'
+  defaults:
+    _entity_form: 'dfp_tag.edit'
+  requirements:
+    _permission: 'administer DFP'
+
+entity.dfp_tag.delete_form:
+  path: '/admin/structure/dfp/tags/manage/{dfp_tag}/delete'
+  defaults:
+    _entity_form: 'dfp_tag.delete'
+    _title: 'Delete'
+  requirements:
+    _permission: 'administer DFP'
+#    _entity_access: 'dfp_tag.delete'
+
+entity.dfp_tag.collection:
+  path: '/admin/structure/dfp/tags'
+  defaults:
+    _entity_list: 'dfp_tag'
+    _title: 'DFP tags'
+  requirements:
+    _permission: 'administer DFP'
diff --git a/dfp.services.yml b/dfp.services.yml
new file mode 100644
index 0000000..d959579
--- /dev/null
+++ b/dfp.services.yml
@@ -0,0 +1,10 @@
+services:
+  dfp.token:
+    class: Drupal\dfp\Token
+    arguments: ['@token', '@current_route_match', '@current_user']
+  dfp.html_response.attachments_processor:
+    public: false
+    class: \Drupal\dfp\DfpHtmlResponseAttachmentsProcessor
+    decorates: html_response.attachments_processor
+    decoration_inner_name: html_response.attachments_processor.original
+    arguments: ['@html_response.attachments_processor.original', '@dfp.token', '@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler']
diff --git a/dfp.tokens.inc b/dfp.tokens.inc
new file mode 100644
index 0000000..abcbaac
--- /dev/null
+++ b/dfp.tokens.inc
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for DFP Ad tag data.
+ */
+
+use Drupal\Core\Render\BubbleableMetadata;
+
+/**
+ * Implements hook_token_info().
+ */
+function dfp_token_info() {
+  $type = [
+    'name' => t('DFP Ad Tags'),
+    'description' => t('Tokens related to a given DFP ad tag.'),
+    'needs-data' => 'dfp_tag',
+  ];
+
+  $tag['slot'] = [
+    'name' => t('Slot Name'),
+    'description' => t("The name of the ad slot defined by this tag."),
+  ];
+  $tag['network_id'] = [
+    'name' => t("Network ID"),
+    'description' => t("The unique ID provided by Google."),
+  ];
+  $tag['ad_categories'] = [
+    'name' => t("DFP Ad Categories"),
+    'description' => t("The DFP Ad Categories or uncategorized taxonomy terms attached to the entities currently being displayed to the user."),
+  ];
+
+  return [
+    'types' => ['dfp_tag' => $type],
+    'tokens' => ['dfp_tag' => $tag],
+  ];
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function dfp_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+  $replacements = [];
+
+  if ($type == 'dfp_tag') {
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'slot':
+          if (!empty($data['dfp_tag'])) {
+            $replacements[$original] = $data['dfp_tag']->getSlot();
+          }
+          break;
+
+        case 'network_id':
+          $config = \Drupal::config('dfp.settings');
+          $replacements[$original] = $config->get('network_id');
+          $bubbleable_metadata->addCacheableDependency($config);
+          break;
+
+        case 'ad_categories':
+          // @todo What to do here?
+          $term_names = &drupal_static('dfp_entity_targeting_terms', []);
+          $replacements[$original] = implode(',', $term_names);
+          break;
+      }
+    }
+  }
+
+  return $replacements;
+}
diff --git a/docs/images/background_ad_options.png b/docs/images/background_ad_options.png
deleted file mode 100644
index cfa1740..0000000
Binary files a/docs/images/background_ad_options.png and /dev/null differ
diff --git a/docs/images/global_key_value_pairs.png b/docs/images/global_key_value_pairs.png
deleted file mode 100644
index 0be49fd..0000000
Binary files a/docs/images/global_key_value_pairs.png and /dev/null differ
diff --git a/docs/images/inject_javascript.png b/docs/images/inject_javascript.png
deleted file mode 100644
index effc3b7..0000000
Binary files a/docs/images/inject_javascript.png and /dev/null differ
diff --git a/docs/images/moderation_state_dropdown.png b/docs/images/moderation_state_dropdown.png
deleted file mode 100644
index 0d302c4..0000000
Binary files a/docs/images/moderation_state_dropdown.png and /dev/null differ
diff --git a/docs/images/taxonomy_options.png b/docs/images/taxonomy_options.png
deleted file mode 100644
index 52bccce..0000000
Binary files a/docs/images/taxonomy_options.png and /dev/null differ
diff --git a/dfp.admin.js b/js/dfp.admin.js
similarity index 100%
rename from dfp.admin.js
rename to js/dfp.admin.js
diff --git a/plugins/content_types/dfp_pane.inc b/plugins/content_types/dfp_pane.inc
deleted file mode 100644
index 55cff62..0000000
--- a/plugins/content_types/dfp_pane.inc
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-/**
- * @file
- * The 'DFP Panel' custom content type.
- */
-
-/**
- * Callback function to supply a list of content types.
- */
-$plugin = array(
-  'title' => t('DFP Pane'),
-  'description' => t('Custom Implementation for DFP Ads.'),
-  'category' => t('Custom'),
-  'single' => TRUE,
-  'edit form' => 'dfp_dfp_pane_content_type_edit_form',
-  'render callback' => 'dfp_dfp_pane_render',
-);
-
-/**
- * Output function for the 'DFP Panel' content type.
- */
-function dfp_dfp_pane_render($subtype, $conf, $panel_args, $context) {
-  $content = array();
-
-  foreach ($conf['dfp_ads'] as $key => $ad) {
-    if (!empty($ad)) {
-      $content[$key] = dfp_tag($ad);
-    }
-  }
-
-  $block = new stdClass();
-  $block->content = $content;
-  return $block;
-}
-
-/**
- * Returns an edit form for the custom type.
- */
-function dfp_dfp_pane_content_type_edit_form($form, &$form_state) {
-  $tags = dfp_tag_load_all();
-
-  $tag_options = array();
-  foreach ($tags as $tag) {
-    $tag_options[$tag->machinename] = $tag->slot;
-  }
-
-  $form['dfp_ads'] = array(
-    '#type' => 'checkboxes',
-    '#options' => $tag_options,
-    '#default_value' => $form_state['conf']['dfp_ads'],
-    '#title' => t('Choose the ad slots to show in this location.'),
-  );
-
-  return $form;
-}
-
-/**
- * Submit function for the content type.
- */
-function dfp_dfp_pane_content_type_edit_form_submit(&$form, &$form_state) {
-  $form_state['conf']['dfp_ads'] = $form_state['values']['dfp_ads'];
-}
diff --git a/plugins/contexts/dfp_context_reaction_adunit.inc b/plugins/contexts/dfp_context_reaction_adunit.inc
deleted file mode 100644
index 5c89a42..0000000
--- a/plugins/contexts/dfp_context_reaction_adunit.inc
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-/**
- * @file
- * Context reaction plugin for DFP ads.
- */
-
-/**
- * Expose DFP tags as context reactions.
- */
-class dfp_context_reaction_adunit extends context_reaction {
-  /**
-   * Allow admins to choose what DFP adunit to override.
-   */
-  function options_form($context) {
-    $form = array();
-
-    // Get existing values for this form.
-    $values = $this->fetch_from_context($context);
-
-    // Get a list of all DART tags.
-    $options = array();
-    $tags = dfp_tag_load_all();
-
-    foreach ($tags as $tag) {
-      $options[$tag->machinename] = $tag->slot;
-    }
-
-    $used_tags = implode(', ', $options);
-
-    $form['adunit_override_tags'] = array(
-      '#type' => 'checkboxes',
-      '#title' => t('Tags to override'),
-      '#description' => t("When this context is active, the tags selected here will have their ad unit overridden."),
-      '#options' => $options,
-      '#default_value' => isset($values['adunit_override_tags']) ? $values['adunit_override_tags'] : array(),
-    );
-    $form['adunit_override'] = array(
-      '#title' => t('Override these DFP tags with a custom ad unit'),
-      '#description' => t('Use the tokens below to define how the ad unit should display. The network id will be included automatically. Example: [dfp_tag:url_parts:4]/[dfp_tag:slot]'),
-      '#type' => 'textfield',
-      '#size' => 60,
-      '#default_value' => isset($values['adunit_override']) ? $values['adunit_override'] : '',
-    );
-    $form['tokens'] = array(
-      '#theme' => 'token_tree',
-      '#token_types' => array('dfp_tag', 'node', 'term', 'user'),
-      '#global_types' => TRUE,
-      '#click_insert' => TRUE,
-      '#dialog' => TRUE,
-    );
-
-    return $form;
-  }
-
-  /**
-   * override all tags based on context.
-   */
-  function execute(&$tag) {
-    // Check each currently set context to see if the DART tag specified by
-    // machinename should be overridden or not.
-    foreach ($this->get_contexts() as $context_name => $context) {
-      if (isset($context->reactions['dfp_adunit'])) {
-      $tags = $context->reactions['dfp_adunit']['adunit_override_tags'];
-      if (isset($tags[$tag->machinename]) && !empty($tags[$tag->machinename])) {
-        $tag->adunit = $context->reactions['dfp_adunit']['adunit_override'];
-      }
-      break;
-      }
-    }
-  }
-}
diff --git a/plugins/contexts/dfp_context_reaction_outofpage.inc b/plugins/contexts/dfp_context_reaction_outofpage.inc
deleted file mode 100644
index d1239ec..0000000
--- a/plugins/contexts/dfp_context_reaction_outofpage.inc
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-/**
- * @file
- * Context reaction plugin for DFP Out of page.
- */
-
-/**
- * Expose DFP Out of page as context reactions.
- */
-class dfp_context_reaction_outofpage extends context_reaction {
-  /**
-   * Allow admins to choose what tags show.
-   */
-  function options_form($context) {
-    // Get existing values for this form.
-    $values = $this->fetch_from_context($context);
-
-    // Get a list of tags.
-    $options = array();
-    $tags = dfp_tag_load_all();
-    foreach ($tags as $tag) {
-      if (!empty($tag->settings['out_of_page'])) {
-        $options[$tag->machinename] = $tag->slot;
-      }
-    }
-
-    $form = array(
-      '#title' => t('Show these DFP Out of page tags'),
-      '#description' => t('The following DFP tags will be added to the page.'),
-      '#default_value' => isset($values) ? $values : array(),
-    );
-    if (!empty($options)) {
-      $form['#type'] = 'checkboxes';
-      $form['#options'] = $options;
-    }
-    else {
-      $form['#markup'] = t(
-        'There are no "Out of page" slots available. You can create them on the !link page.',
-        array(
-          '!link' => l(t('DFP Ad Tags'), 'admin/structure/dfp_ads', array('html' => TRUE)),
-        )
-      );
-    }
-
-    return $form;
-  }
-
-  /**
-   * Disable any tags that should be disabled based on context.
-   */
-  function execute() {
-    // Check each currently set context to see if the DFP tag specified by
-    // machinename should be displayed or not.
-    foreach ($this->get_contexts() as $context_name => $context) {
-      if (isset($context->reactions['dfp_outofpage'])) {
-        foreach ($context->reactions['dfp_outofpage'] as $key => $value) {
-          if (!empty($value)) {
-            $this->out_of_page_tags[$key] = $value;
-          }
-        }
-        break;
-      }
-    }
-  }
-}
diff --git a/plugins/contexts/dfp_context_reaction_settings.inc b/plugins/contexts/dfp_context_reaction_settings.inc
deleted file mode 100644
index f4ccf40..0000000
--- a/plugins/contexts/dfp_context_reaction_settings.inc
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-/**
- * @file
- * Context reaction plugin for DFP ads.
- */
-
-/**
- * Expose DFP variables as context reactions.
- */
-class dfp_context_reaction_settings extends context_reaction {
-  /**
-   * Allow admins to choose what DART tags to hide.
-   */
-  function options_form($context) {
-    module_load_include('inc', 'dfp', 'dfp.admin');
-
-    // Get existing values for this form.
-    $values = $this->fetch_from_context($context);
-
-    // Build the targeting form.
-    $form = array();
-    _dfp_targeting_form($form, isset($values['targeting']) ? $values['targeting'] : array());
-
-    // Modify the #parents property to make sure values are stored in the
-    // location that contexts expects them.
-    $parents = array('reactions', 'plugins', 'dfp_settings');
-    foreach (element_children($form['targeting']) as $key) {
-      if (is_numeric($key)) {
-        $form['targeting'][$key]['target']['#parents'] = array_merge($parents, $form['targeting'][$key]['target']['#parents']);
-        $form['targeting'][$key]['value']['#parents'] = array_merge($parents, $form['targeting'][$key]['value']['#parents']);
-      }
-    }
-    // Modify form to call the contexts-specific versions of certain functions.
-    $form['targeting']['dfp_more_targets']['#submit'] = array('dfp_contexts_more_targets_submit');
-    $form['targeting']['dfp_more_targets']['#ajax']['callback'] = 'dfp_contexts_more_targets_js';
-
-    return $form;
-  }
-
-  /**
-   * Return a keyed array containing the settings.
-   */
-  function execute(&$tag) {
-    foreach ($this->get_contexts() as $context_name => $context) {
-      foreach ($context->reactions['dfp_settings']['targeting'] as $key => $value) {
-        $tag->settings['targeting'][] = $value;
-      }
-    }
-  }
-}
-
-/**
- * Submit handler to add more targets to an ad tag.
- */
-function dfp_contexts_more_targets_submit($form, &$form_state) {
-  $form_state['targeting'] = $form_state['input']['reactions']['plugins']['dfp_settings']['targeting'];
-  $form_state['rebuild'] = TRUE;
-}
-
-/**
- * Ajax callback for add more targets to an ad tag.
- */
-function dfp_contexts_more_targets_js($form, $form_state) {
-  return $form['reactions']['plugins']['dfp_settings']['targeting'];
-}
-
diff --git a/plugins/contexts/dfp_context_reaction_sizes.inc b/plugins/contexts/dfp_context_reaction_sizes.inc
deleted file mode 100644
index b40b8db..0000000
--- a/plugins/contexts/dfp_context_reaction_sizes.inc
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-/**
- * @file
- * Context reaction plugin for DFP ads.
- */
-
-/**
- * Expose DFP tags as context reactions.
- */
-class dfp_context_reaction_sizes extends context_reaction {
-  /**
-   * Allow admins to choose what DFP adunit to override.
-   */
-  function options_form($context) {
-    $form = array(
-      '#theme' => 'dfp_size_settings',
-    );
-
-    // Get existing values for this form.
-    $values = $this->fetch_from_context($context);
-
-    // Get a list of all DART tags.
-    $tags = dfp_tag_load_all();
-    foreach ($tags as $tag) {
-      $form[$tag->machinename . '_size'] = array(
-        '#type' => 'textfield',
-        '#title' => check_plain($tag->slot),
-        '#title_display' => 'invisible',
-        '#description' => t('Example: 300x600,300x250'),
-        '#default_value' => isset($values[$tag->machinename . '_size']) ? $values[$tag->machinename . '_size'] : '',
-      );
-    }
-
-    return $form;
-  }
-
-  /**
-   * override all tags based on context.
-   */
-  function execute(&$tag) {
-    // Check each currently set context to see if the DART tag specified by
-    // machinename should be overridden or not.
-    foreach ($this->get_contexts() as $context_name => $context) {
-      $new_size = isset($context->reactions['dfp_adsize'][$tag->machinename . '_size']) ? $context->reactions['dfp_adsize'][$tag->machinename . '_size'] : '';
-      $tag->size = !empty($new_size) ? $new_size : $tag->size;
-    }
-  }
-}
diff --git a/plugins/contexts/dfp_context_reaction_tags.inc b/plugins/contexts/dfp_context_reaction_tags.inc
deleted file mode 100644
index dbe0661..0000000
--- a/plugins/contexts/dfp_context_reaction_tags.inc
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-/**
- * @file
- * Context reaction plugin for DFP ads.
- */
-
-/**
- * Expose DFP tags as context reactions.
- */
-class dfp_context_reaction_tags extends context_reaction {
-  /**
-   * Allow admins to choose what DFP tags to hide.
-   */
-  function options_form($context) {
-    // Get existing values for this form.
-    $values = $this->fetch_from_context($context);
-
-    // Get a list of all DART tags.
-    $options = array();
-    $tags = dfp_tag_load_all();
-    foreach ($tags as $tag) {
-      $options[$tag->machinename] = $tag->slot;
-    }
-
-    $form = array(
-      '#title' => t('Hide these DFP tags'),
-      '#description' => t('The following DFP tags will not be displayed.'),
-      '#type' => 'checkboxes',
-      '#options' => $options,
-      '#default_value' => isset($values) ? $values : array(),
-    );
-
-    return $form;
-  }
-
-  /**
-   * Disable any tags that should be disabled based on context.
-   */
-  function execute(&$tag) {
-    // Check each currently set context to see if the DART tag specified by
-    // machinename should be displayed or not.
-    foreach ($this->get_contexts() as $context_name => $context) {
-      if (isset($context->reactions['dfp_tags']) && in_array($tag->machinename, $context->reactions['dfp_tags'], TRUE)) {
-        $tag->disabled = TRUE;
-        break;
-      }
-    }
-  }
-}
diff --git a/plugins/export_ui/dfp_ctools_export_ui.inc b/plugins/export_ui/dfp_ctools_export_ui.inc
deleted file mode 100644
index 2b79b62..0000000
--- a/plugins/export_ui/dfp_ctools_export_ui.inc
+++ /dev/null
@@ -1,268 +0,0 @@
-<?php
-
-/**
- * @file
- * A Ctools Export UI plugin for DFP ads.
- */
-
-/**
- * Define this Export UI plugin.
- */
-$plugin = array(
-  'schema' => 'dfp_tags',
-  'access' => 'administer DFP',
-
-  // Define the menu item.
-  'menu' => array(
-    'menu item' => 'dfp_ads',
-    'menu prefix' => 'admin/structure',
-    'menu title' => 'DFP Ad Tags',
-    'menu description' => 'Create and manage your DFP ad tags.',
-  ),
-
-  // Define user interface texts.
-  'title singular' => t('DFP ad'),
-  'title plural' => t('DFP ads'),
-  'title singular proper' => t('DFP ad'),
-  'title plural proper' => t('DFP ads'),
-
-  // Define the class to use as a handler for DFP ad tags.
-  'handler' => array(
-     'class' => 'dfp_tag_ui',
-     'parent' => 'ctools_export_ui',
-  ),
-
-  // Define the names of the functions that provide the add/edit forms.
-
-  'form' => array(
-    'settings' => 'dfp_tag_form',
-    'validate' => 'dfp_tag_form_validate',
-  ),
-);
-
-/**
- * Form builder; Create an ad tag.
- */
-function dfp_tag_form(&$form, &$form_state) {
-  // Make sure that all the necessary files are included even on ajax callbacks.
-  form_load_include($form_state, 'inc', 'dfp', 'plugins/export_ui/dfp_ctools_export_ui');
-  form_load_include($form_state, 'inc', 'dfp', 'dfp.admin');
-
-  // Create a default tag object.
-  $tag = ($form_state['op'] == 'add' ? $form_state['item'] : $form_state['item']->raw);
-
-  $form['settings'] = array(
-    '#type' => 'vertical_tabs',
-  );
-
-  // Tag settings.
-  $form['tag_settings'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Tag Settings'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#group' => 'settings',
-    '#attached' => array(
-      'js' => array(
-        'vertical-tabs' => drupal_get_path('module', 'dfp') . '/dfp.admin.js',
-      ),
-    ),
-  );
-
-  $form['tag_settings']['slot'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Ad Slot Name'),
-    '#required' => TRUE,
-    '#maxlength' => 64,
-    '#default_value' => $tag->slot,
-    '#description' => t('Example: leaderboard or box1'),
-  );
-  $form['tag_settings']['machinename'] = array(
-    '#type' => 'machine_name',
-    '#title' => t('Unique Name'),
-    '#maxlength' => 128,
-    '#default_value' => $tag->machinename,
-    '#description' => t('Only use letters, numbers and underscores. Example: top-banner'),
-    '#machine_name' => array(
-      'exists' => 'dfp_tag_name_exists',
-      'source' => array('tag_settings', 'slot'),
-    ),
-  ) + $form['info']['machinename'];
-  unset($form['info']);
-  $form['tag_settings']['out_of_page'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Out of page (interstitial) ad slot'),
-    '#description' => t('Use Context module to place the Ad slot on the page.'),
-    '#default_value' => isset($tag->settings['out_of_page']) ? $tag->settings['out_of_page'] : 0,
-    '#parents' => array('settings', 'out_of_page'),
-    '#weight' => 0,
-  );
-  $form['tag_settings']['size'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Size(s)'),
-    '#description' => t('Example: 300x600,300x250. For Out Of Page slots, use 0x0'),
-    '#required' => TRUE,
-    '#maxlength' => 64,
-    '#default_value' => $tag->size,
-  );
-  $form['tag_settings']['adunit'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Ad Unit Pattern'),
-    '#required' => FALSE,
-    '#maxlength' => 255,
-    '#default_value' => $tag->adunit,
-    '#description' => t('Use the tokens below to define how the ad unit should display. The network id will be included automatically. Example: [dfp_tag:url_parts:4]/[dfp_tag:slot]. Leave this field empty to use the default ad unit pattern as defined in <a href="@url">Global DFP Settings</a>.', array('@url' => 'admin/structure/dfp_ads/settings')),
-  );
-  $form['tag_settings']['tokens'] = array(
-    '#theme' => 'token_tree',
-    '#token_types' => array('dfp_tag', 'node', 'term', 'user'),
-    '#global_types' => TRUE,
-    '#click_insert' => TRUE,
-    '#dialog' => TRUE,
-  );
-
-  // Global Display settings.
-  $form['tag_display_options'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Display Options'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#group' => 'settings',
-  );
-  $form['tag_display_options']['slug'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Slug'),
-    '#description' => t('Override the default slug for this ad tag. Use !none for no slug. Leave this field empty to use the default slug. Example: Advertisement', array('!none' => check_plain('<none>'))),
-    '#required' => FALSE,
-    '#maxlength' => 64,
-    '#default_value' => $tag->settings['slug'],
-    '#parents' => array('settings', 'slug'),
-  );
-  $form['tag_display_options']['block'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Create a block for this ad tag'),
-    '#description' => t('Display this ad in a block configurable at !url.', array('!url' => l(t('admin/structure/block'), 'admin/structure/block'))),
-    '#default_value' => $tag->block,
-  );
-  $form['tag_display_options']['short_tag'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Render this tag without javascript'),
-    '#description' => t('Use this option for ads included in emails.'),
-    '#default_value' => $tag->settings['short_tag'],
-    '#parents' => array('settings', 'short_tag'),
-  );
-
-  // Responsive settings.
-  $form['breakpoint_settings'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Responsive Settings'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#group' => 'settings',
-  );
-  $existing_breakpoints = _dfp_breakpoints_get_existing($form_state, isset($tag->settings['breakpoints']) ? $tag->settings['breakpoints'] : array());
-  _dfp_breakpoints_form($form['breakpoint_settings'], $existing_breakpoints);
-
-  // Targeting options.
-  $form['targeting_settings'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Targeting'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#group' => 'settings',
-  );
-  $existing_targeting = _dfp_targeting_get_existing($form_state, isset($tag->settings['targeting']) ? $tag->settings['targeting'] : array());
-  _dfp_targeting_form($form['targeting_settings'], $existing_targeting);
-
-  // Backfill ad settings options.
-  $form['backfill_settings'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Backfill Ad Settings'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#group' => 'settings',
-  );
-  $form['backfill_settings']['adsense_ad_types'] = array(
-    '#type' => 'select',
-    '#title' => t('AdSense Ad Type'),
-    '#empty_option' => t('- None -'),
-    '#empty_value' => '',
-    '#default_value' => $tag->settings['adsense_ad_types'],
-    '#options' => array(
-      'text_image' => t('Both image and text ads'),
-      'image' => t('Only image ads'),
-      'text' => t('Only text ads'),
-    ),
-    '#parents' => array('settings', 'adsense_ad_types'),
-    '#description' => t('Choose what type of ads this tag can display when AdSense ads are used for backfill.'),
-  );
-  $form['backfill_settings']['adsense_channel_ids'] = array(
-    '#type' => 'textfield',
-    '#title' => t('AdSense Channel ID(s)'),
-    '#default_value' => $tag->settings['adsense_channel_ids'],
-    '#required' => FALSE,
-    '#description' => t('Example: 271828183+314159265'),
-    '#parents' => array('settings', 'adsense_channel_ids'),
-    '#states' => array(
-      '!visible' => array(
-        array(':input[name="settings[adsense_ad_types]"]' => array('value' => '')),
-      ),
-    )
-  );
-  $form['backfill_settings']['adsense_color_settings'] = array(
-    '#type' => 'container',
-    '#attributes' => array('class' => array('form-item')),
-    '#theme' => 'dfp_adsense_color_settings',
-    '#states' => array(
-      'visible' => array(
-        array(':input[name="settings[adsense_ad_types]"]' => array('value' => 'text')),
-        array(':input[name="settings[adsense_ad_types]"]' => array('value' => 'text_image')),
-      ),
-    ),
-    '#attached' => array(
-      'js' => array(
-        'vertical-tabs' => drupal_get_path('module', 'dfp') . '/dfp.admin.js',
-      ),
-    ),
-  );
-  $adsense_color_settings = array(
-    'background' => t('Background color'),
-    'border' => t('Border color'),
-    'link' => t('Link color'),
-    'text' => t('Text color'),
-    'url' => t('URL color'),
-  );
-  foreach ($adsense_color_settings as $setting => $title) {
-    $form['backfill_settings']['adsense_color_settings'][$setting] = array(
-      '#type' => 'textfield',
-      '#title' => check_plain($title),
-      '#attributes' => array('class' => array('color-setting')),
-      '#field_prefix' => '#',
-      '#title_display' => 'invisible',
-      '#default_value' => isset($tag->settings['adsense_colors'][$setting]) ? $tag->settings['adsense_colors'][$setting] : '',
-      '#size' => 6,
-      '#parents' => array('settings', 'adsense_colors', $setting),
-    );
-  }
-}
-
-/**
- * Form validation for the dfp_ad_tag form.
- */
-function dfp_tag_form_validate(&$form, &$form_state) {
-  // Ad Unit names can only have letters, numbers and slashes.¬
-  if (preg_match('@[^a-zA-Z0-9\/\-_\.\[\]\:]+@', $form_state['values']['adunit'])) {
-    form_set_error('adunit', t('Ad Unit Patterns can only include letters, numbers, hyphens, dashes, periods, slashes and tokens.'));
-  }
-}
-
-/**
- * Check if the given machinename is unique in the dfp_tags table.
- */
-function dfp_tag_name_exists($machinename) {
-  $select = db_select('dfp_tags', 'dt');
-  $select->addExpression('COUNT(*)');
-  $select->condition('dt.machinename', $machinename);
-
-  return $select->execute()->fetchField();
-}
diff --git a/plugins/export_ui/dfp_tag_ui.class.php b/plugins/export_ui/dfp_tag_ui.class.php
deleted file mode 100644
index 04992eb..0000000
--- a/plugins/export_ui/dfp_tag_ui.class.php
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-
-/**
- * @file
- * A custom Ctools Export UI class for DFP Tags.
- */
-
-/**
- * Customizations of the DART Tags UI.
- */
-class dfp_tag_ui extends ctools_export_ui {
-
-  /**
-   * Prepare the tag values before they are added to the database.
-   */
-  function edit_form_submit(&$form, &$form_state) {
-    // Since the targeting form is reusable it isn't already in the settings
-    // array so we grab it here.
-    $form_state['values']['settings']['targeting'] = $form_state['values']['targeting'];
-    $form_state['values']['settings']['breakpoints'] = $form_state['values']['breakpoints'];
-
-    parent::edit_form_submit($form, $form_state);
-  }
-
-  /**
-   * Build a row based on the item.
-   *
-   * By default all of the rows are placed into a table by the render
-   * method, so this is building up a row suitable for theme('table').
-   * This doesn't have to be true if you override both.
-   */
-  function list_build_row($item, &$form_state, $operations) {
-    // Warn users if network id has no value. This can happen immediatly after
-    // the module is installed.
-    if (variable_get('dfp_network_id', '') == '') {
-      drupal_set_message(t('DFP ad tags will not work until you have <a href="/admin/structure/dfp_ads/settings">set your network id</a> as provided by Google.'), 'warning', FALSE);
-    }
-
-    // Set up sorting
-    $name = $item->{$this->plugin['export']['key']};
-    $schema = ctools_export_get_schema($this->plugin['schema']);
-
-    switch ($form_state['values']['order']) {
-      case 'disabled':
-        $this->sorts[$name] = empty($item->disabled) . $name;
-        break;
-      case 'name':
-        $this->sorts[$name] = $name;
-        break;
-      case 'storage':
-        $this->sorts[$name] = $item->{$schema['export']['export type string']} . $name;
-        break;
-    }
-
-    $this->rows[$name]['data'] = array();
-    $this->rows[$name]['class'] = !empty($item->disabled) ? array('ctools-export-ui-disabled') : array('ctools-export-ui-enabled');
-    $this->rows[$name]['data'][] = array('data' => check_plain($item->slot), 'class' => array('ctools-export-ui-slot'));
-    $this->rows[$name]['data'][] = array('data' => check_plain($item->size), 'class' => array('ctools-export-ui-size'));
-    $this->rows[$name]['data'][] = array('data' => (check_plain($item->block) ? t('Yes') : t('No')), 'class' => array('ctools-export-ui-block'));
-    $this->rows[$name]['data'][] = array('data' => check_plain($item->{$schema['export']['export type string']}), 'class' => array('ctools-export-ui-storage'));
-
-    $ops = theme('links__ctools_dropbutton', array('links' => $operations, 'attributes' => array('class' => array('links', 'inline'))));
-
-    $this->rows[$name]['data'][] = array('data' => $ops, 'class' => array('ctools-export-ui-operations'));
-  }
-
-  /**
-   * Provide the table header.
-   *
-   * If you've added columns via list_build_row() but are still using a
-   * table, override this method to set up the table header.
-   */
-  function list_table_header() {
-    $header = array();
-
-    $header[] = array('data' => t('Ad Slot'), 'class' => array('ctools-export-ui-slot'));
-    $header[] = array('data' => t('Size'), 'class' => array('ctools-export-ui-size'));
-    $header[] = array('data' => t('Block'), 'class' => array('ctools-export-ui-block'));
-    $header[] = array('data' => t('Storage'), 'class' => array('ctools-export-ui-storage'));
-    $header[] = array('data' => t('Operations'), 'class' => array('ctools-export-ui-operations'));
-
-    return $header;
-  }
-
-  /**
-   * Make certain that setting form_state['rebuild'] = TRUE in a submit function
-   * will correctly rebuild the exportables item edit form for the user. This
-   * function is needed until the patch at http://drupal.org/node/1524598 is
-   * committed.
-   */
-  function edit_execute_form_standard(&$form_state) {
-    $output = drupal_build_form('ctools_export_ui_edit_item_form', $form_state);
-
-    if (!empty($form_state['executed']) && !$form_state['rebuild']) {
-      // Interstitial slots are not displayed as a block.
-      if (!empty($form_state['values']['settings']['out_of_page'])) {
-        $form_state['item']->block = '0';
-      }
-      $this->edit_save_form($form_state);
-    }
-    else {
-      unset($form_state['executed']);
-    }
-    return $output;
-  }
-
-  /**
-   * Deletes any blocks associated with the exportable item being deleted.
-   */
-  function delete_page($js, $input, $item) {
-    $delta = drupal_strlen($item->machinename) >= 32 ? md5($item->machinename) : $item->machinename;
-    db_delete('block')
-      ->condition('module', 'dfp')
-      ->condition('delta', $delta)
-      ->execute();
-
-    return parent::delete_page($js, $input, $item);
-  }
-
-}
diff --git a/src/DfpHtmlResponseAttachmentsProcessor.php b/src/DfpHtmlResponseAttachmentsProcessor.php
new file mode 100644
index 0000000..e475f61
--- /dev/null
+++ b/src/DfpHtmlResponseAttachmentsProcessor.php
@@ -0,0 +1,177 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\DfpResponseAttachmentsProcessor.
+ */
+
+namespace Drupal\dfp;
+
+use Drupal\Core\Asset\AssetCollectionRendererInterface;
+use Drupal\Core\Asset\AssetResolverInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\EnforcedResponseException;
+use Drupal\Core\Render\AttachmentsInterface;
+use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Render\HtmlResponseAttachmentsProcessor;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\dfp\Entity\TagInterface;
+use Drupal\dfp\View\TagView;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Processes attachments of HTML responses with Dfp slot attachments.
+ *
+ * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
+ * @see \Drupal\dfp\View\TagViewBuilder
+ */
+class DfpHtmlResponseAttachmentsProcessor extends HtmlResponseAttachmentsProcessor {
+
+  /**
+   * The HTML response attachments processor service.
+   *
+   * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface
+   */
+  protected $htmlResponseAttachmentsProcessor;
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The DFP token service.
+   *
+   * @var \Drupal\dfp\TokenInterface
+   */
+  protected $token;
+
+  /**
+   * Constructs a DfpResponseAttachmentsProcessor object.
+   *
+   * @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $html_response_attachments_processor
+   *   The HTML response attachments processor service.
+   * @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
+   *   An asset resolver.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   A config factory for retrieving required config objects.
+   * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer
+   *   The CSS asset collection renderer.
+   * @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_collection_renderer
+   *   The JS asset collection renderer.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
+   */
+  public function __construct(AttachmentsResponseProcessorInterface $html_response_attachments_processor, TokenInterface $token, AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
+    $this->htmlResponseAttachmentsProcessor = $html_response_attachments_processor;
+    $this->configFactory = $config_factory;
+    $this->token = $token;
+    parent::__construct($asset_resolver, $config_factory, $css_collection_renderer, $js_collection_renderer, $request_stack, $renderer, $module_handler);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processAttachments(AttachmentsInterface $response) {
+    // @todo Convert to assertion once https://www.drupal.org/node/2408013 lands
+    if (!$response instanceof HtmlResponse) {
+      throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.');
+    }
+
+    // First, render the actual placeholders. This may add attachments so this
+    // is a bit of unfortunate but necessary duplication.
+    // This is copied verbatim from
+    // \Drupal\Core\Render\HtmlResponseAttachmentsProcessor::processAttachments.
+    try {
+      $response = $this->renderPlaceholders($response);
+    }
+    catch (EnforcedResponseException $e) {
+      return $e->getResponse();
+    }
+
+    // Extract DFP slots; HtmlResponseAttachmentsProcessor does not
+    // know (nor need to know) how to process those.
+    $attachments = $response->getAttachments();
+
+    if (isset($attachments['dfp_slot'])) {
+      $attachments['html_head'][] = [
+        $this->getHeadTop(),
+        'dfp-js-head-top',
+      ];
+
+      /** @var \Drupal\dfp\View\TagView $tag */
+      foreach ($attachments['dfp_slot'] as $tag_view) {
+        $attachments['html_head'][] = [
+          [
+            // Use a fake #type to prevent
+            // HtmlResponseAttachmentsProcessor::processHead() adding one.
+            '#type' => 'dfp_script',
+            '#theme' => 'dfp_slot_definition_js',
+            '#tag' => $tag_view,
+          ],
+          'dfp-slot-definition-' . $tag_view->id(),
+        ];
+      }
+
+      $attachments['html_head'][] = [
+        $this->getHeadBottom(),
+        'dfp-js-head-bottom',
+      ];
+
+      unset($attachments['dfp_slot']);
+    }
+    $response->setAttachments($attachments);
+
+    // Call HtmlResponseAttachmentsProcessor to process all other attachments.
+    return $this->htmlResponseAttachmentsProcessor->processAttachments($response);
+  }
+
+  /**
+   * Gets the javascript to add before the slot definitions.
+   *
+   * @return \Drupal\Component\Render\MarkupInterface
+   *   The rendered HTML.
+   */
+  protected function getHeadTop() {
+    return [
+      // Use a fake #type to prevent
+      // HtmlResponseAttachmentsProcessor::processHead() adding one.
+      '#type' => 'dfp_script',
+      '#theme' => 'dfp_js_head_top',
+      '#google_tag_services_url' => TagInterface::GOOGLE_TAG_SERVICES_URL,
+    ];
+  }
+
+  /**
+   * Gets the javascript to add after the slot definitions.
+   *
+   * @return \Drupal\Component\Render\MarkupInterface
+   *   The rendered HTML.
+   */
+  protected function getHeadBottom() {
+    $global_settings = $this->configFactory->get('dfp.settings');
+    $targeting = $global_settings->get('targeting');
+    $this->moduleHandler->alter('dfp_global_targeting', $targeting);
+    $targeting = TagView::formatTargeting($targeting, $this->token, $this->moduleHandler);
+    return [
+      // Use a fake #type to prevent
+      // HtmlResponseAttachmentsProcessor::processHead() adding one.
+      '#type' => 'dfp_script',
+      '#theme' => 'dfp_js_head_bottom',
+      '#async_rendering' => $global_settings->get('async_rendering'),
+      '#single_request' => $global_settings->get('single_request'),
+      '#collapse_empty_divs' => $global_settings->get('collapse_empty_divs'),
+      '#disable_init_load' => $global_settings->get('disable_init_load'),
+      '#targeting' => $targeting,
+    ];
+  }
+
+}
diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php
new file mode 100644
index 0000000..56a0d1d
--- /dev/null
+++ b/src/Entity/Tag.php
@@ -0,0 +1,245 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Entity\Tag.
+ */
+
+namespace Drupal\dfp\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+
+/**
+ * Defines the DFP Ad tag configuration entity class.
+ *
+ * @todo list_cache_tags
+ *
+ * @ConfigEntityType(
+ *   id = "dfp_tag",
+ *   config_prefix = "tag",
+ *   label = @Translation("DFP tag"),
+ *   handlers = {
+ *     "form" = {
+ *       "add" = "Drupal\dfp\Form\Tag",
+ *       "edit" = "Drupal\dfp\Form\Tag",
+ *       "delete" = "Drupal\Core\Entity\EntityDeleteForm"
+ *     },
+ *     "list_builder" = "Drupal\dfp\View\TagList",
+ *     "view_builder" = "Drupal\dfp\View\TagViewBuilder",
+ *   },
+ *   links = {
+ *     "edit-form" = "/admin/structure/dfp_ads/tags/manage/{tag}",
+ *     "delete-form" = "/admin/structure/dfp_ads/tags/manage/{tag}/delete",
+ *     "collection" = "/admin/structure/dfp_ads/tags",
+ *   },
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "slot"
+ *   },
+ *   admin_permission = "Administer DFP",
+ *   list_cache_tags = { "rendered" },
+ *   config_export = {
+ *     "id",
+ *     "slot",
+ *     "size",
+ *     "adunit",
+ *     "slug",
+ *     "block",
+ *     "short_tag",
+ *     "breakpoints",
+ *     "targeting",
+ *     "adsense_backfill"
+ *   },
+ * )
+ */
+class Tag extends ConfigEntityBase implements TagInterface {
+
+  /**
+   * The unique tag ID.
+   *
+   * Limited to d7 machine name regex.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The ad slot name.
+   *
+   * @var string
+   */
+  protected $slot;
+
+  /**
+   * Out of page (interstitial) ad slot.
+   *
+   * Use Context module to place the Ad slot on the page.
+   *
+   * @var boolean
+   *
+   * @todo Not used at present. Regression from D7.
+   */
+  protected $out_of_page;
+
+  /**
+   * Size(s).
+   *
+   * Example: 300x600,300x250. For Out Of Page slots, use 0x0
+   *
+   * @var string
+   */
+  protected $size;
+
+  /**
+   * The default Brightcove player to use with this client.
+   *
+   * Ad Unit Patterns can only include letters, numbers, hyphens, dashes,
+   * periods, slashes and tokens.
+   *
+   * @var string
+   */
+  protected $adunit;
+
+  /**
+   * Slug.
+   *
+   * Override the default slug for this ad tag. Use <none> for no slug. Leave
+   * this field empty to use the default slug. Example: Advertisement.
+   *
+   * @var string
+   */
+  protected $slug;
+
+  /**
+   * Create a block for this ad tag.
+   *
+   * @var boolean
+   */
+  protected $block = TRUE;
+
+  /**
+   * Render this tag without javascript.
+   *
+   * Use this option for ads included in emails.
+   *
+   * @var boolean
+   */
+  protected $short_tag = FALSE;
+
+  /**
+   * The breakpoints.
+   *
+   * @var array
+   */
+  protected $breakpoints = [];
+
+  /**
+   * The ad targeting.
+   *
+   * @var array
+   */
+  protected $targeting = [];
+
+  /**
+   * Settings used when AdSense ads are used for backfill.
+   *
+   * @var array
+   */
+  protected $adsense_backfill = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function slot() {
+    return $this->label();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function size() {
+    return $this->size;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function adunit() {
+    return $this->adunit;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function slug() {
+    return $this->slug;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasBlock() {
+    return $this->block;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function shortTag() {
+    return $this->short_tag;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function targeting() {
+    return $this->targeting;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function breakpoints() {
+    return $this->breakpoints;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function adsenseAdTypes() {
+    return isset($this->adsense_backfill['ad_types']) ? $this->adsense_backfill['ad_types'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function adsenseChannelIds() {
+    return isset($this->adsense_backfill['channel_ids']) ? $this->adsense_backfill['channel_ids'] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function adsenseColor($setting) {
+    return isset($this->adsense_backfill['color'][$setting]) ? $this->adsense_backfill['color'][$setting] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function adsenseColors() {
+    return isset($this->adsense_backfill['color']) ? $this->adsense_backfill['color'] : [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage, $update);
+
+    // Invalidate the block cache to update DFP ad tag-based derivatives.
+    \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
+  }
+
+}
diff --git a/src/Entity/TagInterface.php b/src/Entity/TagInterface.php
new file mode 100644
index 0000000..fb6f006
--- /dev/null
+++ b/src/Entity/TagInterface.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Entity\TagInterface.
+ */
+
+namespace Drupal\dfp\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * An interface for the DFP Ad tag configuration entity.
+ */
+interface TagInterface extends ConfigEntityInterface {
+
+  const ADUNIT_PATTERN_VALIDATION_REGEX = '@[^a-zA-Z0-9\/\-_\.\[\]\:]+@';
+  const ADSENSE_TEXT_IMAGE = 'text_image';
+  const ADSENSE_IMAGE = 'image';
+  const ADSENSE_TEXT = 'text';
+  const GOOGLE_SHORT_TAG_SERVICES_URL = 'pubads.g.doubleclick.net/gampad';
+  const GOOGLE_TAG_SERVICES_URL = 'www.googletagservices.com/tag/js/gpt.js';
+
+  /**
+   * Gets the ad slot.
+   *
+   * @return string
+   *   The ad slot. This is the same as the label for the configuration entity.
+   */
+  public function slot();
+
+  /**
+   * Gets the ad size or sizes.
+   *
+   * @return string
+   *   The ad size or sizes. Example: 300x600,300x250.
+   */
+  public function size();
+
+  /**
+   * Gets ad unit pattern.
+   *
+   * @return string
+   *   The ad unit pattern. May contain tokens.
+   */
+  public function adunit();
+
+  /**
+   * Gets the slug.
+   *
+   * @return string
+   *   The slug.
+   */
+  public function slug();
+
+  /**
+   * Determines whether the tag provides a block plugin.
+   *
+   * @return bool
+   *   TRUE if the tag provides a block plugin, FALSE if not.
+   *
+   * @see \Drupal\dfp\Plugin\Derivative\TagBlock
+   */
+  public function hasBlock();
+
+  /**
+   * Determines whether to display the tag as a short tag.
+   *
+   * @return bool
+   *   TRUE to display the tag as a short tag, FALSE if not.
+   */
+  public function shortTag();
+
+  /**
+   * Gets the ad targeting.
+   *
+   * @return array[]
+   *   Each value is a array containing two keys: 'target' and 'value'. Both
+   *   values are strings. Multiple value values are delimited by a comma.
+   */
+  public function targeting();
+
+  /**
+   * Gets the type of ads displayed when AdSense ads are used for backfill.
+   *
+   * @return string
+   *   The type of ads displayed when AdSense ads are used for backfill.
+   */
+  public function adsenseAdTypes();
+
+  /**
+   * Gets the Adsense channel ID(s) when AdSense ads are used for backfill.
+   *
+   * @return string
+   *   The Adsense channel ID(s) when AdSense ads are used for backfill.
+   *   Multiple IDs are delimited by a + sign.
+   */
+  public function adsenseChannelIds();
+
+  /**
+   * Gets the color for a setting when AdSense ads are used for backfill.
+   *
+   * @param string $setting
+   *   The setting to get the color for. Either: 'background', 'border', 'link',
+   *   'text' or 'url'.
+   *
+   * @return string
+   *   The color for a setting when AdSense ads are used for backfill. For
+   *   example: FFFFFF.
+   */
+  public function adsenseColor($setting);
+
+  /**
+   * Gets the colors used when AdSense ads are used for backfill.
+   *
+   * @return string[]
+   *   An array keyed by setting with hex colors as values.
+   */
+  public function adsenseColors();
+
+  /**
+   * Gets the breakpoints.
+   *
+   * @return array[]
+   *   Each value is a array containing two keys: 'browser_size' and 'ad_sizes'.
+   *   The 'browser_size' is a value such as '1024x768'. The 'ad_sizes' value
+   *   contains a list of ad sizes to be be used at this 'browser_size'.
+   */
+  public function breakpoints();
+
+}
diff --git a/src/Form/AdminSettings.php b/src/Form/AdminSettings.php
new file mode 100644
index 0000000..a1855c1
--- /dev/null
+++ b/src/Form/AdminSettings.php
@@ -0,0 +1,290 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Form\AdminSettings.
+ */
+
+namespace Drupal\dfp\Form;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\dfp\Entity\TagInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Defines a form that configures DFP global settings.
+ */
+class AdminSettings extends ConfigFormBase {
+  use TargetingFormTrait;
+
+  /**
+   * Entity bundle information.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $bundleInfo;
+
+  /**
+   * Constructs a \Drupal\dfp\Form\AdminSettings object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, EntityTypeBundleInfoInterface $bundle_info) {
+    parent::__construct($config_factory);
+    $this->bundleInfo = $bundle_info;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('entity_type.bundle.info')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'dfp_admin_settings_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'dfp.settings',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, Request $request = NULL) {
+    $config = $this->config('dfp.settings');
+
+    // @todo vertical tabs?
+
+    // Default tag settings.
+    $form['global_tag_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Global Tag Settings'),
+      '#open' => TRUE,
+    ];
+    $form['global_tag_settings']['network_id'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Network ID'),
+      '#default_value' => $config->get('network_id'),
+      '#required' => TRUE,
+      '#description' => t('The Network ID to use on all tags. According to Google this value should begin with a /.'),
+    ];
+    $form['global_tag_settings']['adunit_pattern'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Default Ad Unit Pattern'),
+      '#required' => FALSE,
+      '#maxlength' => 255,
+      '#default_value' => $config->get('adunit_pattern'),
+      '#description' => $this->t('Use the tokens below to define how the default ad unit should display. This can be overridden on each tag. The network id will be included automatically. Example: [current-page:url:args:value:0]/[current-page:url:args:value:1]/[dfp_tag:slot]'),
+    ];
+    $form['global_tag_settings']['click_url'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('DFP Click URL (Sync mode only)'),
+      '#default_value' => $config->get('click_url'),
+      '#description' => $this->t('Sets a click URL on each DFP tag. Useful for intercepting ad click calls for reporting. Values beginning with http:// are treated as absolute; otherwise relative to current site domain.'),
+      '#states' => [
+        'enabled' => [
+          'input[name="async_rendering"]' => ['checked' => FALSE],
+        ],
+      ],
+    ];
+    // @todo: Add back token browser.
+
+    $form['global_tag_settings']['async_rendering'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Render ads asynchronously'),
+      '#default_value' => $config->get('async_rendering'),
+      '#description' => $this->t('This can speed up page rendering time by loading page content without waiting for ads to load.'),
+    ];
+    $form['global_tag_settings']['disable_init_load'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Disable initial ad load'),
+      '#default_value' => $config->get('disable_init_load'),
+      '#description' => $this->t('(Async mode only) Disables the initial fetch of ads from Google when the page is first loaded. Calls to refresh() can still be used to fetch ads.'),
+      '#states' => [
+        'enabled' => [
+          'input[name="async_rendering"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['global_tag_settings']['single_request'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Combine all ad requests into a single request'),
+      '#default_value' => $config->get('single_request'),
+      '#description' => $this->t('This can speed up page rendering time by limiting the number of external requests.'),
+    ];
+
+    $ad_categories_bundles = $config->get('ad_categories_bundles');
+    $form['global_tag_settings']['enable_ad_categories'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable DFP Ad Categories'),
+      '#default_value' => !empty($ad_categories_bundles),
+      '#description' => $this->t('Example: if you have an "animals" vocabulary and you want to target the same ads to "dogs", "cats" and "hamsters" you can edit each of those terms and set the DFP Ad Category to "pets". Whenever the taxonomy terms are included as targeting values, anything tagged "cats" will target "pets" instead.'),
+    ];
+
+    $bundles = $this->bundleInfo->getBundleInfo('taxonomy_term');
+    $options = [];
+    foreach ($bundles as $key => $bundle) {
+      if ($key != 'dfp_ad_categories') {
+        $options[$key] = (string) $bundle['label'];
+      }
+    }
+    // @todo react to bundle
+    $form['global_tag_settings']['ad_categories_bundles'] = [
+      '#type' => 'checkboxes',
+      '#options' => $options,
+      '#default_value' => $ad_categories_bundles,
+      '#title' => $this->t('Select the vocabularies on which DFP Ad Categories should be enabled.'),
+      '#states' => [
+        'visible' => [
+          'input[name="enable_ad_categories"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+
+    // Global display options.
+    $form['global_display_options'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Global Display Options'),
+      '#open' => TRUE,
+    ];
+    $form['global_display_options']['default_slug'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Global Slug'),
+      '#default_value' => $config->get('default_slug'),
+      '#required' => FALSE,
+      '#description' => $this->t('Slug all ad tags with this label. Example: Advertisement'),
+    ];
+    $form['global_display_options']['collapse_empty_divs'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Collapse empty divs'),
+      '#default_value' => $config->get('collapse_empty_divs'),
+      '#options' => [
+        0 => $this->t('Never'),
+        1 => $this->t('Collapse only if no ad is served'),
+        2 => $this->t('Expand only if an ad is served'),
+      ],
+      '#description' => $this->t('<dl><dt>Never</dt><dd>Never collapse ad slots.</dd><dt>Collapse only</dt><dd>Collapse before any ad is loaded. Useful if ad slots will get filled most of the time.</dd><dt>Expand only</dt><dd>Collapse all divs on the page before any ads are fetched and expand if an ad is loaded into the ad slot. Useful if ad slots will stay empty most of the time.</dd></dl>'),
+    ];
+    $form['global_display_options']['hide_slug'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Hide slug if no ad is served (recommended)'),
+      '#default_value' => $config->get('hide_slug'),
+      '#states' => [
+        'visible' => [
+          'input[name="collapse_empty_divs"]' => ['!value' => 0],
+        ],
+      ],
+    ];
+
+    // Global targeting options.
+    $form['targeting_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Global Targeting'),
+      '#open' => TRUE,
+    ];
+    $existing_targeting = $this->getExistingTargeting($form_state, $config->get('targeting'));
+    $this->addTargetingForm($form['targeting_settings'], $existing_targeting);
+
+    // AdTest Settings.
+    $form['adtest'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Ad Test Settings'),
+      '#open' => TRUE,
+    ];
+    $form['adtest']['adtest_adunit_pattern'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Ad Unit Pattern for Ad Tests'),
+      '#description' => $this->t('Override the Ad Unit value for all the ad tags on a page by adding ?adtest=true to the URL. Use the tokens below to define how the ad unit should display. Example: [dfp_tag:network_id]/test/[dfp_tag:slot]'),
+      '#default_value' => $config->get('adtest_adunit_pattern'),
+    ];
+    // @todo Add back token browser.
+    // @todo Drupal 7 also had the ability to inject Javascript. Discuss whether
+    //   or not this should be implemented.
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    if ($form_state->getValue('click_url') && $form_state->getValue('async_rendering')) {
+      $form_state->setErrorByName('click_url', $this->t('Setting a click URL does not work with async rendering.'));
+    }
+    if (preg_match(TagInterface::ADUNIT_PATTERN_VALIDATION_REGEX, $form_state->getValue('adunit_pattern'))) {
+      $form_state->setErrorByName('adunit_pattern', $this->t('Ad Unit Patterns can only include letters, numbers, hyphens, dashes, periods, slashes and tokens.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+    if (!$values['enable_ad_categories']) {
+      $values['ad_categories_bundles'] = [];
+    }
+    if (!$values['async_rendering']) {
+      $values['disable_init_load'] = FALSE;
+    }
+
+    // @todo Add fields to selected vocabularies.
+    // Attach (or delete) an instance of the dfp_ad_categories term_reference
+    // field for each vocabulary that should (or should not) have DFP Ad
+    // Categories enabled.
+    //    foreach ($form_state['values']['dfp_enable_ad_categories_bundles'] as $bundle => $enable) {
+    //      $existing_instance = field_read_instance('taxonomy_term', 'field_dfp_ad_categories', $bundle);
+    //      $enable = $enable && !$existing_instance && $form_state['values']['dfp_enable_ad_categories'];
+    //      if ($enable) {
+    //        $instance = array(
+    //          'field_name' => 'field_dfp_ad_categories',
+    //          'entity_type' => 'taxonomy_term',
+    //          'label' => t('DFP Ad Category'),
+    //          'bundle' => $bundle,
+    //          'required' => FALSE,
+    //          'widget' => array(
+    //            'type' => 'options_select'
+    //          ),
+    //        );
+    //        field_create_instance($instance);
+    //      }
+    //      elseif (!$enable && $existing_instance) {
+    //        // Delete this field instance, but be certain not to delete the field.
+    //        field_delete_instance($existing_instance, FALSE);
+    //      }
+    //    }
+
+    $this->config('dfp.settings')
+      ->set('network_id', $values['network_id'])
+      ->set('adunit_pattern', $values['adunit_pattern'])
+      ->set('click_url', $values['click_url'])
+      ->set('async_rendering', $values['async_rendering'])
+      ->set('disable_init_load', $values['disable_init_load'])
+      ->set('single_request', $values['single_request'])
+      ->set('ad_categories_bundles', $values['ad_categories_bundles'])
+      ->set('default_slug', $values['default_slug'])
+      ->set('collapse_empty_divs', $values['collapse_empty_divs'])
+      ->set('adtest_adunit_pattern', $values['adtest_adunit_pattern'])
+      ->set('targeting', $values['targeting'])
+      ->set('hide_slug', $values['hide_slug'])
+      ->save();
+  }
+
+}
diff --git a/src/Form/BreakpointFormTrait.php b/src/Form/BreakpointFormTrait.php
new file mode 100644
index 0000000..4b0bbfc
--- /dev/null
+++ b/src/Form/BreakpointFormTrait.php
@@ -0,0 +1,180 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Form\BreakpointFormTrait.
+ */
+
+namespace Drupal\dfp\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides form for adding breakpoints to a DFP tag.
+ */
+trait BreakpointFormTrait {
+
+  /**
+   * Validation function used by an individual breakpoint.
+   */
+  public static function breakpointFormValidate(array $element, FormStateInterface &$form_state) {
+    if (empty($element['browser_size']['#value']) && !empty($element['ad_sizes']['#value'])) {
+      $form_state->setError($element['browser_size'], t('The browser size cannot be empty if ad size(s) exists.'));
+    }
+    elseif (!empty($element['browser_size']['#value']) && empty($element['ad_sizes']['#value'])) {
+      $form_state->setError($element['ad_sizes'], t('The ad size(s) cannot be empty if a browser size exists.'));
+    }
+    if (!empty($element['browser_size']['#value']) && !empty($element['ad_sizes']['#value'])) {
+      if (preg_match('/[^x|0-9]/', $element['browser_size']['#value'])) {
+        $form_state->setError($element['browser_size'], t('The browser size can only contain numbers and the character x.'));
+      }
+      elseif (preg_match('/[^x|,|0-9]/', $element['ad_sizes']['#value'])) {
+        $form_state->setError($element['ad_sizes'], t('The ad size(s) can only contain numbers, the character x and commas.'));
+      }
+    }
+  }
+
+  /**
+   * Validation function used by the breakpoints form.
+   */
+  public static function breakpointsFormValidate(array &$element, FormStateInterface &$form_state) {
+    if ($form_state->getTriggeringElement()['#name'] != 'dfp_more_breakpoints') {
+      self::breakpointsTrim($form_state->getValues());
+    }
+  }
+
+  /**
+   * Helper function that removes empty breakpoints from form values.
+   */
+  protected static function breakpointsTrim(array &$values, $parent = 'breakpoints') {
+    foreach ($values as $key => &$val) {
+      if ($key === $parent) {
+        // We found the browser_size values.
+        foreach ($val as $k => $v) {
+          if (empty($val[$k]['browser_size']) && empty($val[$k]['ad_sizes'])) {
+            unset($val[$k]);
+          }
+        }
+        // Reset the array indexes to prevent wierd behavior caused by a
+        // breakpoint being removed in the middle of the array.
+        $val = array_values($val);
+        break;
+      }
+      elseif (is_array($val)) {
+        self::breakpointsTrim($val, $parent);
+      }
+    }
+  }
+
+  /**
+   * Submit handler to add more breakpoints to an ad tag.
+   */
+  public function moreBreakpointsSubmit(array $form, FormStateInterface &$form_state) {
+    $form_state->setValue('breakpoints', $form_state->getUserInput()['breakpoints']);
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Ajax callback for adding breakpoints to the breakpoint form.
+   */
+  public function moreBreakpointsJs(array $form, FormStateInterface $form_state) {
+    return $form['breakpoint_settings']['breakpoints'];
+  }
+
+  /**
+   * Helper form builder for the breakpoints form.
+   */
+  protected function addBreakpointsForm(array &$breakpoints_form, array $existing_breakpoints = []) {
+    // Display settings.
+    $breakpoints_form['breakpoints'] = [
+      '#type' => 'markup',
+      '#tree' => FALSE,
+      '#prefix' => '<div id="dfp-breakpoints-wrapper">',
+      '#suffix' => '</div>',
+      '#element_validate' => [[get_class($this), 'breakpointsFormValidate']],
+    ];
+
+    $breakpoints_form['breakpoints']['table'] = [
+      '#type' => 'table',
+      '#header' => [
+        $this->t('Browser Size'),
+        $this->t('Ad Size(s)'),
+      ],
+    ];
+
+    $breakpoints_form['breakpoints']['help'] = [
+      '#type' => 'markup',
+      '#prefix' => '<p>',
+      '#markup' => $this->t('These breakpoints are set to implement DFP responsive mappings. See <a href="https://support.google.com/dfp_premium/answer/3423562?hl=en">this support article</a> for more information.'),
+      '#suffix' => '</p>',
+    ];
+
+    // Add existing breakpoints to the form unless they are empty.
+    foreach ($existing_breakpoints as $key => $data) {
+      $this->addBreakpointForm($breakpoints_form, $key, $data);
+    }
+    // Add one blank set of breakpoint fields.
+    $this->addBreakpointForm($breakpoints_form, count($existing_breakpoints));
+
+    $breakpoints_form['breakpoints']['dfp_more_breakpoints'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Add another breakpoint'),
+      '#submit' => [[get_class($this), 'moreBreakpointsSubmit']],
+      '#limit_validation_errors' => [],
+      '#ajax' => [
+        'callback' => [get_class($this), 'moreBreakpointsJs'],
+        'wrapper' => 'dfp-breakpoints-wrapper',
+        'effect' => 'fade',
+      ],
+    ];
+  }
+
+  /**
+   * Helper form builder for an individual breakpoint.
+   */
+  protected function addBreakpointForm(array &$form, $key, array $data = []) {
+    $form['breakpoints']['table'][$key] = [
+      '#prefix' => '<div class="breakpoint" id="breakpoint-' . $key . '">',
+      '#suffix' => '</div>',
+      '#element_validate' => [[get_class($this), 'breakpointFormValidate']],
+    ];
+    $form['breakpoints']['table'][$key]['browser_size'] = [
+      '#type' => 'textfield',
+      '#title_display' => 'invisible',
+      '#title' => $this->t('Minimum Browser Size'),
+      '#size' => 10,
+      '#default_value' => isset($data['browser_size']) ? $data['browser_size'] : '',
+      '#parents' => ['breakpoints', $key, 'browser_size'],
+      '#attributes' => ['class' => ['field-breakpoint-browser-size']],
+    ];
+    $form['breakpoints']['table'][$key]['ad_sizes'] = [
+      '#type' => 'textfield',
+      '#title_display' => 'invisible',
+      '#title' => $this->t('Ad Sizes'),
+      '#size' => 20,
+      '#default_value' => isset($data['ad_sizes']) ? $data['ad_sizes'] : '',
+      '#parents' => ['breakpoints', $key, 'ad_sizes'],
+      '#attributes' => ['class' => ['field-breakpoint-ad-sizes']],
+    ];
+    if (empty($data)) {
+      $form['breakpoints']['table'][$key]['browser_size']['#description'] = $this->t('Example: 1024x768');
+      $form['breakpoints']['table'][$key]['ad_sizes']['#description'] = $this->t('Example: 300x600,300x250');
+    }
+  }
+
+  /**
+   * Returns the current breakpoints.
+   *
+   * @return array
+   *   The default value will be used unless an "input" exists in the form_state
+   *   variable, in which case that will be used.
+   */
+  protected function getExistingBreakpoints(FormStateInterface $form_state, array $breakpoints = []) {
+    $user_input = $form_state->getUserInput();
+    if (isset($user_input['breakpoints'])) {
+      $breakpoints = $user_input['breakpoints'];
+    }
+    return $breakpoints;
+  }
+
+}
diff --git a/src/Form/Tag.php b/src/Form/Tag.php
new file mode 100644
index 0000000..5993713
--- /dev/null
+++ b/src/Form/Tag.php
@@ -0,0 +1,223 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Form\Tag.
+ */
+
+namespace Drupal\dfp\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\dfp\Entity\TagInterface;
+
+/**
+ * Form to edit and add DFP tags.
+ */
+class Tag extends EntityForm {
+  use BreakpointFormTrait;
+  use TargetingFormTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    // @todo Implement vertical tabs like D7 module.
+    // @todo Implement out_of_page setting like D7 module.
+    $form = parent::form($form, $form_state);
+
+    /** @var \Drupal\dfp\Entity\TagInterface $tag */
+    $tag = $this->entity;
+    if ($this->operation == 'add') {
+      $form['#title'] = $this->t('Add DFP tag');
+    }
+    else {
+      $form['#title'] = $this->t('Edit %label DFP tag', ['%label' => $tag->label()]);
+    }
+
+    // Tag settings.
+    $form['tag_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Tag Settings'),
+      '#open' => TRUE,
+    ];
+
+    $form['tag_settings']['slot'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Ad Slot Name'),
+      '#required' => TRUE,
+      '#maxlength' => 64,
+      '#default_value' => $tag->slot(),
+      '#description' => $this->t('Example: leaderboard or box1'),
+    ];
+
+    $form['tag_settings']['id'] = [
+      '#type' => 'machine_name',
+      '#maxlength' => 128,
+      '#default_value' => $tag->id(),
+      '#description' => $this->t('A unique machine-readable name for this DFP tag. Only use letters, numbers and underscores. Example: top_banner'),
+      '#machine_name' => [
+        'exists' => ['Drupal\dfp\Entity\Tag', 'load'],
+        'source' => ['tag_settings', 'slot'],
+      ],
+    ];
+
+    $form['tag_settings']['size'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Size(s)'),
+      '#description' => $this->t('Example: 300x600,300x250. For Out Of Page slots, use 0x0'),
+      '#required' => TRUE,
+      '#maxlength' => 64,
+      '#default_value' => $tag->size(),
+    ];
+    $form['tag_settings']['adunit'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Ad Unit Pattern'),
+      '#required' => FALSE,
+      '#maxlength' => 255,
+      '#default_value' => $tag->adunit(),
+      '#description' => $this->t('Use the tokens below to define how the ad unit should display. The network id will be included automatically. Example: [dfp_tag:url_parts:4]/[dfp_tag:slot]. Leave this field empty to use the default ad unit adunit as defined in <a href=":url">Global DFP Settings</a>.', [':url' => Url::fromRoute('dfp.admin_settings')->toString()]),
+    ];
+    // @todo Add token browser.
+
+    // Global Display settings.
+    $form['tag_display_options'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Display Options'),
+      '#open' => TRUE,
+    ];
+    $form['tag_display_options']['slug'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Slug'),
+      '#description' => $this->t('Override the default slug for this ad tag. Use @none for no slug. Leave this field empty to use the default slug. Example: Advertisement', ['@none' => '<none>']),
+      '#required' => FALSE,
+      '#maxlength' => 64,
+      '#default_value' => $tag->slug(),
+    ];
+    $form['tag_display_options']['block'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Create a block for this ad tag'),
+      '#description' => $this->t('Display this ad in a block configurable. <a href=":url">Place the block</a>.', [':url' => Url::fromRoute('block.admin_display')->toString()]),
+      '#default_value' => $tag->hasBlock(),
+    ];
+    $form['tag_display_options']['short_tag'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Render this tag without javascript'),
+      '#description' => $this->t('Use this option for ads included in emails.'),
+      '#default_value' => $tag->shortTag(),
+    ];
+
+    // Responsive settings.
+    $form['breakpoint_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Responsive Settings'),
+      '#open' => TRUE,
+    ];
+    $existing_breakpoints = $this->getExistingBreakpoints($form_state, $tag->breakpoints());
+    $this->addBreakpointsForm($form['breakpoint_settings'], $existing_breakpoints);
+
+    // Targeting options.
+    $form['targeting_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Targeting'),
+      '#open' => TRUE,
+    ];
+    $existing_targeting = $this->getExistingTargeting($form_state, $tag->targeting());
+    $this->addTargetingForm($form['targeting_settings'], $existing_targeting);
+
+    // Backfill ad settings options.
+    $form['adsense_backfill'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Backfill Ad Settings'),
+      '#open' => TRUE,
+      '#tree' => TRUE,
+    ];
+    $form['adsense_backfill']['ad_types'] = [
+      '#type' => 'select',
+      '#title' => $this->t('AdSense Ad Type'),
+      '#empty_option' => $this->t('- None -'),
+      '#empty_value' => '',
+      '#default_value' => $tag->adsenseAdTypes(),
+      '#options' => [
+        TagInterface::ADSENSE_TEXT_IMAGE => $this->t('Both image and text ads'),
+        TagInterface::ADSENSE_IMAGE => $this->t('Only image ads'),
+        TagInterface::ADSENSE_TEXT => $this->t('Only text ads'),
+      ],
+      '#description' => $this->t('Choose what type of ads this tag can display when AdSense ads are used for backfill.'),
+    ];
+    $form['adsense_backfill']['channel_ids'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('AdSense Channel ID(s)'),
+      '#default_value' => $tag->adsenseChannelIds(),
+      '#required' => FALSE,
+      '#description' => $this->t('Example: 271828183+314159265'),
+      '#states' => [
+        '!visible' => [
+          [':input[name="adsense_backfill[ad_types]"]' => ['value' => '']],
+        ],
+      ],
+    ];
+    $form['adsense_backfill']['color'] = [
+      '#type' => 'fieldgroup',
+      '#title' => $this->t('Color Settings for Text Ads'),
+      '#attributes' => ['class' => ['form-item']],
+      '#states' => [
+        'visible' => [
+          [':input[name="adsense_backfill[ad_types]"]' => ['value' => TagInterface::ADSENSE_TEXT]],
+          [':input[name="adsense_backfill[ad_types]"]' => ['value' => TagInterface::ADSENSE_TEXT_IMAGE]],
+        ],
+      ],
+    ];
+    $adsense_color_settings = [
+      'background' => $this->t('Background color'),
+      'border' => $this->t('Border color'),
+      'link' => $this->t('Link color'),
+      'text' => $this->t('Text color'),
+      'url' => $this->t('URL color'),
+    ];
+    foreach ($adsense_color_settings as $setting => $title) {
+      // @todo integrate color picker if color module enabled.
+      $form['adsense_backfill']['color'][$setting] = [
+        '#type' => 'textfield',
+        '#title' => $title,
+        '#attributes' => ['class' => ['color-setting']],
+        '#field_prefix' => '#',
+        '#default_value' => $tag->adsenseColor($setting),
+        '#size' => 6,
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    if (preg_match(TagInterface::ADUNIT_PATTERN_VALIDATION_REGEX, $form_state->getValue('adunit'))) {
+      $form_state->setErrorByName('adunit', $this->t('Ad Unit Patterns can only include letters, numbers, hyphens, dashes, periods, slashes and tokens.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $tag = $this->entity;
+    $status = $tag->save();
+    $t_args['%slot'] = $tag->label();
+
+    if ($status == SAVED_UPDATED) {
+      drupal_set_message(t('The DFP tag %slot has been updated.', $t_args));
+    }
+    elseif ($status == SAVED_NEW) {
+      drupal_set_message(t('The DFP tag %slot has been added.', $t_args));
+      $context = array_merge($t_args, ['link' => $tag->toLink($this->t('Edit DFP tag'), 'edit-form')->toString()]);
+      $this->logger('dfp')->notice('Added DFP tag %slot.', $context);
+    }
+
+    $form_state->setRedirectUrl($tag->toUrl('collection'));
+  }
+
+}
diff --git a/src/Form/TargetingFormTrait.php b/src/Form/TargetingFormTrait.php
new file mode 100644
index 0000000..6552a32
--- /dev/null
+++ b/src/Form/TargetingFormTrait.php
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Form\TargetingFormTrait.
+ */
+
+namespace Drupal\dfp\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Adds a form for saving DFP targeting information.
+ */
+trait TargetingFormTrait {
+
+  /**
+   * Returns the current targets.
+   *
+   * The default value will be used unless an "input" exists in the form_state
+   * variable, in which case that will be used.
+   */
+  protected function getExistingTargeting(FormStateInterface $form_state, array $targeting = []) {
+    $user_input = $form_state->getUserInput();
+    if (isset($user_input['targeting'])) {
+      $targeting = $user_input['targeting'];
+    }
+    return $targeting;
+  }
+
+  /**
+   * Helper form builder for the targeting form.
+   */
+  protected function addTargetingForm(array &$targeting_form, array $existing_targeting = []) {
+    // Display settings.
+    $targeting_form['targeting'] = [
+      '#type' => 'markup',
+      '#tree' => FALSE,
+      '#prefix' => '<div id="dfp-targeting-wrapper">',
+      '#suffix' => '</div>',
+      '#element_validate' => [[get_class($this), 'targetingFormValidate']],
+    ];
+
+    $targeting_form['targeting']['table'] = [
+      '#type' => 'table',
+      '#header' => [
+        $this->t('Target'),
+        $this->t('Value(s)'),
+      ],
+    ];
+
+    // Add existing targets to the form unless they are empty.
+    foreach ($existing_targeting as $key => $data) {
+      $this->addTargetForm($targeting_form, $key, $data);
+    }
+    // Add one blank set of target fields.
+    $this->addTargetForm($targeting_form, count($existing_targeting));
+
+    $targeting_form['targeting']['dfp_more_targets'] = [
+      '#type' => 'submit',
+      '#value' => t('Add another target'),
+      '#submit' => [get_class($this), 'targetingFormMoreTargetsSubmit'],
+      '#limit_validation_errors' => [],
+      '#ajax' => [
+        'callback' => [get_class($this), 'moreTargetsJs'],
+        'wrapper' => 'dfp-targeting-wrapper',
+        'effect' => 'fade',
+      ],
+    ];
+    // @todo Add token browser.
+  }
+
+  /**
+   * Validation function used by the targeting form.
+   */
+  public static function targetingFormValidate(array &$element, FormStateInterface &$form_state) {
+    if ($form_state->getTriggeringElement()['#name'] != 'dfp_more_targets') {
+      self::trimTargetingValues($form_state->getValues());
+    }
+  }
+
+  /**
+   * Submit handler to add more targets to an ad tag.
+   */
+  public function targetingFormMoreTargetsSubmit(array $form, FormStateInterface &$form_state) {
+    $form_state->set('targeting', $form_state->getUserInput()['targeting']);
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Ajax callback for adding targets to the targeting form.
+   */
+  public function moreTargetsJs(array $form, FormStateInterface $form_state) {
+    return $form['targeting_settings']['targeting'];
+  }
+
+  /**
+   * Helper form builder for an individual target.
+   */
+  protected function addTargetForm(array &$form, $key, array $data = []) {
+    $form['targeting']['table'][$key] = [
+      '#prefix' => '<div class="target" id="target-' . $key . '">',
+      '#suffix' => '</div>',
+      '#element_validate' => [[get_class($this), 'targetFormValidate']],
+    ];
+    $form['targeting']['table'][$key]['target'] = [
+      '#type' => 'textfield',
+      '#title_display' => 'invisible',
+      '#title' => t('Target Name'),
+      '#size' => 10,
+      '#default_value' => isset($data['target']) ? $data['target'] : '',
+      '#parents' => ['targeting', $key, 'target'],
+      '#attributes' => ['class' => ['field-target-target']],
+    ];
+    $form['targeting']['table'][$key]['value'] = [
+      '#type' => 'textfield',
+      '#title_display' => 'invisible',
+      '#title' => t('Target Value'),
+      '#size' => 20,
+      '#default_value' => isset($data['value']) ? $data['value'] : '',
+      '#parents' => ['targeting', $key, 'value'],
+      '#attributes' => ['class' => ['field-target-value']],
+    ];
+    if (empty($data)) {
+      $form['targeting']['table'][$key]['target']['#description'] = t('Example: color');
+      $form['targeting']['table'][$key]['value']['#description'] = t('Example: red,white,blue');
+    }
+  }
+
+  /**
+   * Validation function used by an individual target in the targeting form.
+   */
+  public static function targetFormValidate(array $element, FormStateInterface &$form_state) {
+    if (empty($element['target']['#value']) && !empty($element['value']['#value'])) {
+      $form_state->setError($element['target'], t('The target cannot be empty if a value exists.'));
+    }
+    elseif (!empty($element['target']['#value']) && empty($element['value']['#value'])) {
+      $form_state->setError($element['value'], t('The value cannot be empty if a target exists.'));
+    }
+  }
+
+  /**
+   * Helper function that removes empty targets form form values.
+   */
+  protected static function trimTargetingValues(&$values, $parent = 'targeting') {
+    foreach ($values as $key => &$val) {
+      if ($key === $parent) {
+        // We found the targeting values.
+        foreach ($val as $k => $v) {
+          if (empty($val[$k]['target']) && empty($val[$k]['value'])) {
+            unset($val[$k]);
+          }
+        }
+        // Reset the array indexes to prevent wierd behavior caused by a target
+        // being removed in the middle of the array.
+        $val = array_values($val);
+        break;
+      }
+      elseif (is_array($val)) {
+        self::trimTargetingValues($val, $parent);
+      }
+    }
+  }
+
+}
diff --git a/src/Plugin/Block/TagBlock.php b/src/Plugin/Block/TagBlock.php
new file mode 100644
index 0000000..e7dc403
--- /dev/null
+++ b/src/Plugin/Block/TagBlock.php
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Plugin\Block\TagBlock.
+ */
+
+namespace Drupal\dfp\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\EntityTypeManager;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a generic DFP Ad block type.
+ *
+ * @Block(
+ *  id = "dfp_ad",
+ *  admin_label = @Translation("DFP Ad block"),
+ *  category = @Translation("Advertising"),
+ *  deriver = "Drupal\dfp\Plugin\Derivative\TagBlock"
+ * )
+ */
+class TagBlock extends BlockBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The entity repository service.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManager
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The Drupal account to use for checking for access to block.
+   *
+   * @var \Drupal\Core\Session\AccountInterface.
+   */
+  protected $account;
+
+  /**
+   * Constructs a new BlockContentBlock.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+   *   The entity repository service.
+   * @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository, EntityTypeManager $entity_type_manager, AccountInterface $account) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->entityRepository = $entity_repository;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->account = $account;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity.repository'),
+      $container->get('entity_type.manager'),
+      $container->get('current_user')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'label_display' => 'hidden',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    if ($tag = $this->getEntity()) {
+      return $this->entityTypeManager->getViewBuilder($tag->getEntityTypeId())->view($tag);
+    }
+    else {
+      return [
+        '#markup' => $this->t('DFP tag with uuid %uuid does not exist. <a href=":url">Add DFP tag</a>.', [
+          '%uuid' => $this->getDerivativeId(),
+          ':url' => Url::fromRoute('entity.dfp_tag.add_form')->toString(),
+        ]),
+        '#access' => $this->account->hasPermission('administer DFP'),
+      ];
+    }
+  }
+
+  /**
+   * Loads the block content entity of the block.
+   *
+   * @return \Drupal\dfp\Entity\TagInterface|null
+   *   The block content entity.
+   */
+  protected function getEntity() {
+    $uuid = $this->getDerivativeId();
+    if (!isset($this->tag)) {
+      $this->tag = $this->entityRepository->loadEntityByUuid('dfp_tag', $uuid);
+    }
+    return $this->tag;
+  }
+
+}
diff --git a/src/Plugin/Derivative/TagBlock.php b/src/Plugin/Derivative/TagBlock.php
new file mode 100644
index 0000000..3e761b8
--- /dev/null
+++ b/src/Plugin/Derivative/TagBlock.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Plugin\Derivative\TagBlock.
+ */
+
+namespace Drupal\dfp\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Retrieves block plugin definitions for all available DFP Ad tags.
+ */
+class TagBlock extends DeriverBase implements ContainerDeriverInterface {
+  use StringTranslationTrait;
+
+  /**
+   * The DFP tag storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $tagStorage;
+
+  /**
+   * Constructs a TagBlock object.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $tag_storage
+   *   The DFP tag storage.
+   */
+  public function __construct(EntityStorageInterface $tag_storage) {
+    $this->tagStorage = $tag_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    $entity_type_manager = $container->get('entity_type.manager');
+    return new static(
+      $entity_type_manager->getStorage('dfp_tag')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    /** @var \Drupal\dfp\Entity\TagInterface[] $tags */
+    $tags = $this->tagStorage->loadMultiple();
+    foreach ($tags as $tag) {
+      if ($tag->hasBlock()) {
+        $this->derivatives[$tag->uuid()] = $base_plugin_definition;
+        $this->derivatives[$tag->uuid()]['admin_label'] = $this->t('DFP tag: @slotname', ['@slotname' => $tag->slot()]);
+        $this->derivatives[$tag->uuid()]['config_dependencies']['config'] = [$tag->getConfigDependencyName()];
+      }
+    }
+    return parent::getDerivativeDefinitions($base_plugin_definition);
+  }
+
+}
diff --git a/src/Tests/DfpTestBase.php b/src/Tests/DfpTestBase.php
new file mode 100644
index 0000000..f6169a6
--- /dev/null
+++ b/src/Tests/DfpTestBase.php
@@ -0,0 +1,229 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Tests\DfpTestBase.
+ */
+
+namespace Drupal\dfp\Tests;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Dfp\Entity\Tag;
+use Drupal\dfp\Entity\TagInterface;
+use Drupal\dfp\View\TagView;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * An abstract class to build DFP tests from.
+ */
+abstract class DfpTestBase extends WebTestBase {
+
+  /**
+   * An admin user.
+   *
+   * @var \Drupal\user\Entity\User
+   */
+  protected $adminUser;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['dfp'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Create an admin user with all the permissions needed to run tests.
+    $this->adminUser = $this->drupalCreateUser([
+      'administer DFP',
+      'administer taxonomy',
+      'access administration pages',
+      'administer blocks',
+      'bypass node access',
+      'administer nodes',
+    ]);
+    $this->drupalLogin($this->adminUser);
+
+    // Set up global settings needed for DFP ads to work.
+    \Drupal::configFactory()->getEditable('dfp.settings')
+      ->set('network_id', '12345')
+      ->set('default_slug', 'Global DFP slug')
+      ->save();
+  }
+
+  /**
+   * Creates a basic dfp ad tag.
+   *
+   * @param array $edit
+   *   An array of values for the DFP tag form.
+   *
+   * @return \Drupal\dfp\Entity\Tag
+   *   The created DFP tag.
+   */
+  protected function dfpCreateTag($edit = []) {
+    // Create a new tag.
+    $edit += $this->dfpBasicTagEditValues();
+    $this->drupalPostForm('admin/structure/dfp/tags/add', $edit, t('Save'));
+
+    // Load the tag object.
+    $tag = Tag::load($edit['id']);
+    $this->assertTrue(is_object($tag) && $tag->id() == $edit['id'], 'The new DFP tag was saved correctly.');
+
+    // Display the new tag.
+    $this->drupalPlaceBlock('dfp_ad:' . $tag->uuid());
+
+    return $tag;
+  }
+
+  /**
+   * Edits a given tag specified by $id with the given values.
+   *
+   * @param string $id
+   *   The DFP tag ID.
+   * @param array $edit
+   *   An array of values for the DFP tag form.
+   *
+   * @return \Drupal\dfp\Entity\Tag
+   *   The edited DFP tag.
+   */
+  protected function dfpEditTag($id, &$edit) {
+    // Make sure there is no machinename set when we are editing.
+    if (isset($edit['id'])) {
+      unset($edit['id']);
+    }
+    $this->drupalPostForm('admin/structure/dfp/tags/manage/' . $id, $edit, t('Save'));
+    return Tag::load($id);
+  }
+
+  /**
+   * Converts a DFP Tag config entity to a TagView object.
+   *
+   * @param \Drupal\dfp\Entity\TagInterface $tag
+   *   The DFP tag.
+   *
+   * @return \Drupal\dfp\View\TagView
+   *   The TagView object.
+   */
+  protected function dfpTagToTagView(TagInterface $tag) {
+    return new TagView($tag, $this->getGlobalConfig(), $this->container->get('dfp.token'), $this->container->get('module_handler'));
+  }
+
+  /**
+   * Creates a simple form values $edit array to be used to create a DFP tag.
+   *
+   * @return array
+   *   A simple $edit array to be used on the DFP tag form.
+   */
+  protected function dfpBasicTagEditValues() {
+    $machinename = $this->randomMachineName(16);
+    $basic_tag = [
+      'id' => Unicode::strtolower($machinename),
+      'slot' => $machinename,
+      'size' => implode(',', $this->dfpGenerateSize(2)),
+      'adunit' => $this->randomMachineName(),
+      'block' => 1,
+      'slug' => $this->randomMachineName(32),
+      'adsense_backfill[ad_types]' => '',
+      'adsense_backfill[channel_ids]' => '',
+      'adsense_backfill[color][background]' => '',
+      'adsense_backfill[color][border]' => '',
+      'adsense_backfill[color][link]' => '',
+      'adsense_backfill[color][text]' => '',
+      'adsense_backfill[color][url]' => '',
+      'targeting[0][target]' => $this->randomMachineName(8),
+      'targeting[0][value]' => $this->randomMachineName(8),
+      'breakpoints[0][browser_size]' => $this->dfpGenerateSize(),
+      'breakpoints[0][ad_sizes]' => implode(',', $this->dfpGenerateSize(2)),
+    ];
+
+    return $basic_tag;
+  }
+
+  /**
+   * Generates a random size (or array of sizes) to use when testing tags.
+   *
+   * @param int $count
+   *   How many sizes to generate.
+   *
+   * @return string|array
+   *   A size formatted as ###x### or an array of sizes if $count > 1.
+   */
+  protected function dfpGenerateSize($count = 1) {
+    $sizes = [
+      '300x250', '300x600', '728x90', '728x10', '160x600', '120x80', '300x100',
+      '50x50', '160x300',
+    ];
+    shuffle($sizes);
+
+    return $count == 1 ? array_pop($sizes) : array_slice($sizes, 0, min($count, count($sizes)));
+  }
+
+  /**
+   * Gets the global DFP settings.
+   *
+   * @return \Drupal\Core\Config\ImmutableConfig
+   *   The global DFP settings.
+   */
+  protected function getGlobalConfig() {
+    return \Drupal::config('dfp.settings');
+  }
+
+  /**
+   * Assert that a property is properly being set.
+   *
+   * @param string $property
+   *   The property.
+   * @param string $key
+   *   The key.
+   * @param string $val
+   *   The value.
+   *
+   * @return bool
+   *   TRUE if the property is set, FALSE otherwise.
+   */
+  protected function assertPropertySet($property, $key, $val) {
+    $pattern = $this->getPropertyPattern($property, $key, $val);
+    return $this->assertPattern($pattern, 'A ' . $property . ' property was set for ' . $key . ' = ' . $val);
+  }
+
+  /**
+   * Assert that a property is not being set.
+   *
+   * @param string $property
+   *   The property.
+   * @param string $key
+   *   The key.
+   * @param string $val
+   *   The value.
+   *
+   * @return bool
+   *   TRUE if the property is not set, FALSE otherwise.
+   */
+  protected function assertPropertyNotSet($property, $key, $val) {
+    $pattern = $this->getPropertyPattern($property, $key, $val);
+    return $this->assertNoPattern($pattern, 'A ' . $property . ' property was not set for ' . $key . ' = ' . $val);
+  }
+
+  /**
+   * Gets pattern used in assertPropertySet() and assertPropertyNotSet().
+   *
+   * @param string $property
+   *   The property.
+   * @param string $key
+   *   The key.
+   * @param string $val
+   *   The value.
+   *
+   * @return string
+   *   The pattern.
+   */
+  private function getPropertyPattern($property, $key, $val) {
+    return '|' . '.set' . $property . '\(\'' . $key . '\',{1}\s(.)*' . addslashes($val) . '|';
+  }
+
+}
diff --git a/src/Tests/DisplayTagTest.php b/src/Tests/DisplayTagTest.php
new file mode 100644
index 0000000..261359f
--- /dev/null
+++ b/src/Tests/DisplayTagTest.php
@@ -0,0 +1,210 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Tests\DisplayTagTest.
+ */
+
+namespace Drupal\dfp\Tests;
+
+use Drupal\Component\Utility\Unicode;
+
+/**
+ * Tests display of DFP ad tag.
+ *
+ * @group dfp
+ */
+class DisplayTagTest extends DfpTestBase {
+
+  /**
+   * Tests display a DFP tag in a block.
+   */
+  public function testDisplayTag() {
+    // Create a simple tag as a block.
+    $tag = $this->dfpCreateTag();
+    $tag_view = $this->dfpTagToTagView($tag);
+    $this->drupalGet('<front>');
+    $this->assertRaw('googletag.defineSlot("' . $tag_view->getAdUnit() . '", ' . $tag_view->getSize() . ', "' . $tag_view->getPlaceholderId() . '")');
+
+    // Create a tag with an ID longer than 32 characters.
+    $edit = ['id' => Unicode::strtolower($this->randomMachineName(64))];
+    $tag = $this->dfpCreateTag($edit);
+    $tag_view = $this->dfpTagToTagView($tag);
+    $this->drupalGet('<front>');
+    $this->assertRaw('googletag.defineSlot("' . $tag_view->getAdUnit() . '", ' . $tag_view->getSize() . ', "' . $tag_view->getPlaceholderId() . '")');
+  }
+
+  /**
+   * Tests breakpoint mappings.
+   *
+   * @todo add multiple breakpoint configs to the tag.
+   */
+  public function testDisplayTagWithMapping() {
+    $edit = $this->dfpBasicTagEditValues();
+
+    // Create a simple tag with a mapping. Verify javascript on page.
+    $tag = $this->dfpCreateTag($edit);
+    $this->drupalGet('<front>');
+    $mapping_sizes = explode(',', $edit['breakpoints[0][ad_sizes]']);
+    $size_count = count($mapping_sizes);
+    // Calculate addSize mappings.
+    $mapping_tag = '.addSize(';
+    $mapping_tag .= '[' . str_replace('x', ', ', $edit['breakpoints[0][browser_size]']) . '], ';
+    $mapping_tag .= ($size_count > 1) ? '[' : '';
+    for ($i = 0; $i < $size_count; $i++) {
+      $mapping_sizes[$i] = '[' . str_replace('x', ', ', $mapping_sizes[$i]) . ']';
+      $mapping_tag .= ($i + 1 !== $size_count) ? $mapping_sizes[$i] . ', ' : $mapping_sizes[$i];
+    }
+    $mapping_tag .= ($size_count > 1) ? '])' : ')';
+    $this->assertRaw('googletag.sizeMapping()', 'The ad slot correctly attaches size mapping.');
+    $this->assertRaw('.defineSizeMapping(mapping)', 'The ad slot correctly defines size mapping.');
+    $this->assertRaw($mapping_tag, 'The ad slot correctly defines specific size mappings.');
+
+    // Create a tag with invalid browser size mappings.
+    $edit['breakpoints[0][browser_size]'] = '100y100';
+    $this->dfpEditTag($tag->id(), $edit);
+    $this->assertText(t('The browser size can only contain numbers and the character x.'), 'An error was correctly thrown when invalid characters.');
+
+    // Create a tag with invalid ad size mappings.
+    $edit['breakpoints[0][browser_size]'] = $this->dfpGenerateSize();
+    $edit['breakpoints[0][ad_sizes]'] = '100y100,200x200';
+    $this->dfpEditTag($tag->id(), $edit);
+    $this->assertText(t('The ad size(s) can only contain numbers, the character x and commas.'), 'An error was correctly thrown when invalid characters.');
+  }
+
+  /**
+   * Tests slug display.
+   */
+  public function testSlug() {
+    $this->config('dfp.settings')->set('hide_slug', FALSE)->save();
+    $edit = $this->dfpBasicTagEditValues();
+
+    // Create a tag without a slug, display it and ensure the default slug is
+    // displayed.
+    $edit['slug'] = '';
+    $tag = $this->dfpCreateTag($edit);
+    $this->drupalGet('<front>');
+    $this->assertText('Global DFP slug', 'The default slug is correctly used when no slug exists for an individual tag.');
+
+    // Change the slug to <none> and ensure that no slug is displayed.
+    $edit['slug'] = '<none>';
+    $this->dfpEditTag($tag->id(), $edit);
+    $this->drupalGet('<front>');
+    $this->assertNoText('Global DFP slug', 'No slug is appearing when "<none>" is used.');
+
+    // Specify a slug and check that it shows instead of the default slug.
+    $edit['slug'] = 'Tag specific slug';
+    $this->dfpEditTag($tag->id(), $edit);
+    $this->drupalGet('<front>');
+    $this->assertText('Tag specific slug');
+
+    // Set the slug to be hidden. Use admin UI and the cache tags added in
+    // \Drupal\dfp\View\TagViewBuilder::viewMultiple() are tested.
+    $edit = [
+      'hide_slug' => TRUE,
+    ];
+    $this->drupalPostForm('admin/structure/dfp/settings', $edit, t('Save configuration'));
+    $this->drupalGet('<front>');
+    $this->assertNoText('Tag specific slug');
+  }
+
+  /**
+   * Tests targeting display.
+   */
+  public function testTargeting() {
+    $edit = $this->dfpBasicTagEditValues();
+
+    // Create a tag with a target with only one value.
+    $tag = $this->dfpCreateTag($edit);
+    $this->drupalGet('<front>');
+    $this->assertPropertySet('Targeting', $edit['targeting[0][target]'], $edit['targeting[0][value]']);
+
+    // Create a tag with a target with multiple values.
+    $values = [
+      $this->randomMachineName(),
+      $this->randomMachineName(),
+      $this->randomMachineName(),
+    ];
+    $edit['targeting[0][target]'] = $this->randomMachineName();
+    $edit['targeting[0][value]'] = implode(', ', $values);
+    $this->dfpEditTag($tag->id(), $edit);
+    $this->drupalGet('<front>');
+    $this->assertPropertySet('Targeting', $edit['targeting[0][target]'], implode("','", $values));
+
+    // Create a tag with a target but no value.
+    $edit['targeting[0][target]'] = $this->randomMachineName();
+    $edit['targeting[0][value]'] = '';
+    $this->dfpEditTag($tag->id(), $edit);
+    $this->assertText(t('The value cannot be empty if a target exists.'));
+
+    // Create a tag with an empty target, but a value.
+    $edit['targeting[0][target]'] = '';
+    $edit['targeting[0][value]'] = $this->randomMachineName();
+    $this->dfpEditTag($tag->id(), $edit);
+    $this->assertText(t('The target cannot be empty if a value exists.'));
+
+    // Create a tag with multiple targets.
+    $count = 3;
+    for ($i = 0; $i < $count; $i++) {
+      $edit['targeting[' . $i . '][target]'] = $this->randomMachineName();
+      $edit['targeting[' . $i . '][value]'] = $this->randomMachineName();
+      $this->dfpEditTag($tag->id(), $edit);
+    }
+    $this->drupalGet('<front>');
+    for ($i = 0; $i < $count; $i++) {
+      $this->assertPropertySet('Targeting', $edit['targeting[' . $i . '][target]'], $edit['targeting[' . $i . '][value]']);
+    }
+
+    // Test that target can be removed and does not result in empty values.
+    $old_target = $edit['targeting[0][target]'];
+    $old_value = $edit['targeting[0][value]'];
+    $edit['targeting[0][target]'] = '';
+    $edit['targeting[0][value]'] = '';
+    $this->dfpEditTag($tag->id(), $edit);
+    $this->drupalGet('<front>');
+    $this->assertPropertyNotSet('Targeting', $old_target, $old_value);
+    $this->assertPropertyNotSet('Targeting', '', '');
+
+    // Create a tag that uses the slot token in a target.
+    $edit = $this->dfpBasicTagEditValues();
+    $test_slot = $this->randomMachineName();
+    $edit['slot'] = $test_slot;
+    $edit['targeting[0][target]'] = 'slot';
+    $edit['targeting[0][value]'] = '[dfp_tag:slot]';
+    $this->dfpCreateTag($edit);
+    $this->drupalGet('<front>');
+    $this->assertPropertySet('Targeting', 'slot', $test_slot);
+
+    // Create a tag that uses the network ID token in a target.
+    $edit = $this->dfpBasicTagEditValues();
+    $edit['targeting[0][target]'] = 'network id';
+    $edit['targeting[0][value]'] = '[dfp_tag:network_id]';
+    $this->dfpCreateTag($edit);
+    $this->drupalGet('<front>');
+    $this->assertPropertySet('Targeting', 'network id', '12345');
+  }
+
+  /**
+   * Tests Adsense backfill settings.
+   */
+  public function testBackfill() {
+    $edit = $this->dfpBasicTagEditValues();
+
+    // Create a tag with backfill settings.
+    $colors = ['background', 'border', 'link', 'text', 'url'];
+
+    $edit['adsense_backfill[ad_types]'] = 'text_image';
+    $edit['adsense_backfill[channel_ids]'] = $this->randomMachineName();
+    foreach ($colors as $color) {
+      $edit['adsense_backfill[color][' . $color . ']'] = Unicode::strtoupper($this->randomMachineName(6));
+    }
+    $this->dfpCreateTag($edit);
+    $this->drupalGet('<front>');
+    $this->assertPropertySet('', 'adsense_ad_types', $edit['adsense_backfill[ad_types]']);
+    $this->assertPropertySet('', 'adsense_channel_ids', $edit['adsense_backfill[channel_ids]']);
+    foreach ($colors as $color) {
+      $this->assertPropertySet('', 'adsense_' . $color . '_color', '#' . $edit['adsense_backfill[color][' . $color . ']']);
+    }
+  }
+
+}
diff --git a/src/Tests/GlobalSettingsTest.php b/src/Tests/GlobalSettingsTest.php
new file mode 100644
index 0000000..4150bf7
--- /dev/null
+++ b/src/Tests/GlobalSettingsTest.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Tests\GlobalSettingsTest.
+ */
+
+namespace Drupal\dfp\Tests;
+
+/**
+ * Tests DFP global configuration.
+ *
+ * @group dfp
+ *
+ * @see dfp.settings.yml
+ * @see \Drupal\dfp\Form\AdminSettings
+ */
+class GlobalSettingsTest extends DfpTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['dfp'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $web_user = $this->drupalCreateUser(['administer DFP']);
+    $this->drupalLogin($web_user);
+  }
+
+  /**
+   * Tests \Drupal\dfp\Form\AdminSettings form and dfp_page_attachments().
+   */
+  public function testGlobalSettings() {
+    $edit = [
+      'network_id' => '123456789',
+      'async_rendering' => TRUE,
+      'single_request' => TRUE,
+      'collapse_empty_divs' => '1',
+      'targeting[0][target]' => '<em>test target</em>',
+      'targeting[0][value]' => '<em>test value</em>, test value 2 ',
+    ];
+    $this->drupalPostForm('admin/structure/dfp/settings', $edit, t('Save configuration'));
+
+    $this->drupalGet('<front>');
+    $this->assertNoRaw('googletag', 'With no DFP tags set up there is no additional JS added');
+
+    // Create a tag.
+    $this->dfpCreateTag();
+
+    $this->drupalGet('<front>');
+    $this->assertRaw('googletag.pubads().enableAsyncRendering();', 'Asynchronous rendering is turned on.');
+    $this->assertRaw('googletag.pubads().enableSingleRequest();', 'Single request is turned on.');
+    $this->assertRaw('googletag.pubads().collapseEmptyDivs();', 'Collapse empty divs is turned on.');
+    $this->assertRaw("googletag.pubads().setTargeting('&lt;em&gt;test target&lt;/em&gt;', ['&lt;em&gt;test value&lt;/em&gt;','test value 2']);", 'Global targeting values appear correclty in javascript.');
+
+    $edit = [
+      'network_id' => '123456789',
+      'async_rendering' => FALSE,
+      'single_request' => FALSE,
+      'collapse_empty_divs' => '0',
+      'click_url' => '/custom_click_url',
+      'targeting[0][target]' => 'test target ',
+      'targeting[0][value]' => 'test value 3',
+      'targeting[1][target]' => 'test target 2',
+      'targeting[1][value]' => 'test value 4',
+    ];
+    $this->drupalPostForm('admin/structure/dfp/settings', $edit, t('Save configuration'));
+
+    $this->drupalGet('<front>');
+    $this->assertNoRaw('googletag.pubads().enableAsyncRendering();', 'Asynchronous rendering is turned on.');
+    $this->assertNoRaw('googletag.pubads().enableSingleRequest();', 'Single request is turned on.');
+    $this->assertNoRaw('googletag.pubads().collapseEmptyDivs();', 'Collapse empty divs is turned on.');
+    $this->assertRaw("googletag.pubads().setTargeting('test target', ['test value 3']);", 'Global targeting values appear correctly in javascript.');
+    $this->assertRaw("googletag.pubads().setTargeting('test target 2', ['test value 4']);", 'Global targeting values appear correctly in javascript.');
+    $this->assertEqual('/custom_click_url', \Drupal::config('dfp.settings')->get('click_url'));
+
+    $edit = [
+      'async_rendering' => TRUE,
+      'click_url' => '/custom_click_url',
+      'adunit_pattern' => '$has_an_illegal_character',
+    ];
+    $this->drupalPostForm('admin/structure/dfp/settings', $edit, t('Save configuration'));
+    $this->assertText(t('Setting a click URL does not work with async rendering.'));
+    $this->assertText(t('Ad Unit Patterns can only include letters, numbers, hyphens, dashes, periods, slashes and tokens.'));
+  }
+
+}
diff --git a/src/Tests/dfp.test b/src/Tests/dfp.test
new file mode 100644
index 0000000..7428905
--- /dev/null
+++ b/src/Tests/dfp.test
@@ -0,0 +1,265 @@
+<?php
+
+/**
+ * @file
+ * Test file for DFP module.
+ */
+
+class dfpBaseTest extends DrupalWebTestCase {
+  /**
+   * Implementation of setUp().
+   */
+  function setUp() {
+    // Enable a couple modules.
+    parent::setUp('ctools', 'dfp', 'taxonomy');
+    menu_rebuild();
+
+    // Create an admin user with all the permissions needed to run tests.
+    $this->admin_user = $this->drupalCreateUser(['administer DFP', 'administer taxonomy', 'access administration pages', 'administer blocks', 'bypass node access', 'administer nodes', 'edit terms in 1']);
+    $this->drupalLogin($this->admin_user);
+
+    // Add the some global settings needed for dfp ads to work.
+    variable_set('dfp_network_id', '12345');
+    variable_set('dfp_default_slug', $this->randomName(8));
+  }
+
+  /**
+   * Create a simple $edit array to be used on the DFP tag form to create a new
+   * DFP tag object.
+   */
+  function dfpBasicTagEditValues() {
+    $machinename = drupal_strtolower($this->randomName(8));
+    $basic_tag = [
+      'machinename' => $machinename,
+      'slot' => $machinename,
+      'size' => implode(',', $this->dfpGenerateSize(2)),
+      'adunit' => $this->randomName(8),
+      'block' => 1,
+      'settings[slug]' => $this->randomName(32),
+      'settings[adsense_ad_types]' => '',
+      'settings[adsense_channel_ids]' => '',
+      'settings[adsense_colors][background]' => '',
+      'settings[adsense_colors][border]' => '',
+      'settings[adsense_colors][link]' => '',
+      'settings[adsense_colors][text]' => '',
+      'settings[adsense_colors][url]' => '',
+      'targeting[0][target]' => $this->randomName(8),
+      'targeting[0][value]' => $this->randomName(8),
+      'breakpoints[0][browser_size]' => $this->dfpGenerateSize(),
+      'breakpoints[0][ad_sizes]' => implode(',', $this->dfpGenerateSize(2)),
+    ];
+
+    return $basic_tag;
+  }
+
+  /**
+   * Generate a random size (or array or sizes) to use when testing tags
+   * formatted as ###x###.
+   */
+  function dfpGenerateSize($count = 1) {
+    $sizes = ['300x250', '300x600', '728x90', '728x10', '160x600', '120x80', '300x100', '50x50', '160x300'];
+    shuffle($sizes);
+
+    return $count == 1 ? array_pop($sizes) : array_slice($sizes, 0, min($count, count($sizes)));
+  }
+
+  /**
+   * Creates a basic dfp ad tag.
+   */
+  function createTag($edit = []) {
+    // Create a new tag.
+    $edit += $this->dfpBasicTagEditValues();
+    $this->drupalPost('admin/structure/dfp_ads/add', $edit, t('Save'));
+
+    // Load the tag object.
+    $tag = dfp_tag_load($edit['machinename']);
+    $this->assertTrue(is_object($tag) && $tag->machinename = $edit['machinename'], 'The new tag was saved correctly.');
+
+    // Display the new tag.
+    $edit = [];
+    $delta = 'dfp_' . (drupal_strlen($tag->machinename) >= 32 ? md5($tag->machinename) : $tag->machinename);
+    $edit['blocks[' . $delta . '][region]'] = 'sidebar_first';
+    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+
+    return $tag;
+  }
+
+  /**
+   * Edit a given tag specified by $machinename with the given values.
+   */
+  function editTag($machinename, &$edit) {
+    // Make sure there is no machinename set when we are editing.
+    if (isset($edit['machinename'])) {
+      unset($edit['machinename']);
+    }
+    $this->drupalPost('admin/structure/dfp_ads/list/' . $machinename . '/edit', $edit, t('Save'));
+    $tag = dfp_tag_load($machinename);
+    return $tag;
+  }
+
+  /**
+   * Create a taxonomy term.
+   */
+  function createTerm($vocabulary) {
+    $term = new stdClass();
+    $term->name = $this->randomName();
+    $term->description = $this->randomName();
+    // Use the first available text format.
+    $term->format = db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField();
+    $term->vid = $vocabulary->vid;
+    taxonomy_term_save($term);
+    return $term;
+  }
+
+  /**
+   * Assert that a property is properly being set.
+   */
+  function assertPropertySet($property, $key, $val) {
+    $pattern = '|' . '.set' . $property . '\("' . $key . '",{1}\s(.)*' . addslashes($val) . '|';
+    $this->assertPattern($pattern, 'A ' . $property . ' property was set for ' . $key . ' = ' . $val);
+  }
+
+  /**
+   * Assert that a property is not being set.
+   */
+  function assertPropertyNotSet($property, $key, $val) {
+    $pattern = '|' . '.set' . $property . '\("' . $key . '",{1}\s(.)*' . addslashes($val) . '|';
+    $this->assertNoPattern($pattern, 'A ' . $property . ' property was not set for ' . $key . ' = ' . $val);
+  }
+
+}
+
+class dfpAdCategoriesTest extends dfpBaseTest {
+  /**
+   * Implements getInfo().
+   */
+  public static function getInfo() {
+    return [
+      'name' => t('DFP Ad Categories'),
+      'description' => t('Tests the functionaility of DFP ad categories.'),
+      'group' => t('DFP'),
+    ];
+  }
+
+  function testEnableDisableAdCategories() {
+    // Check that enabling DFP Ad Categories for the "tags" vocabulary works.
+    $edit = [
+      'dfp_enable_ad_categories' => 1,
+      'dfp_enable_ad_categories_bundles[tags]' => 'tags',
+    ];
+    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
+    $this->drupalGet('admin/structure/taxonomy/tags/add');
+    $this->assertFieldByName('field_dfp_ad_categories[und]');
+
+    // Check that disabling DFP Ad Categories vocabulary works even when a
+    // bundle is selected.
+    $edit = [
+      'dfp_enable_ad_categories' => 0,
+    ];
+    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
+    $this->drupalGet('admin/structure/taxonomy/tags/add');
+    $this->assertNoFieldByName('field_dfp_ad_categories[und]');
+
+    // Check that disabling DFP Ad Categories on a specific vocabulary works.
+    // For this test to be valid, we first need to reenable DFP Ad Categories.
+    $edit = [
+      'dfp_enable_ad_categories' => 1,
+      'dfp_enable_ad_categories_bundles[tags]' => 'tags',
+    ];
+    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
+    $this->drupalGet('admin/structure/taxonomy/tags/add');
+    $edit = [
+      'dfp_enable_ad_categories_bundles[tags]' => 0,
+    ];
+    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
+    $this->drupalGet('admin/structure/taxonomy/tags/add');
+    $this->assertNoFieldByName('field_dfp_ad_categories[und]');
+  }
+
+  // Test that DFP Catetories are displayed properly.
+  function testDisplayAdCategories() {
+    $dfp_add_categories_vocabulary = taxonomy_vocabulary_machine_name_load('dfp_ad_categories');
+    $tags_vocabulary = taxonomy_vocabulary_machine_name_load('tags');
+
+    // Turn on DFP ad categories for the "tags" vocabulary.
+    $edit = [
+      'dfp_enable_ad_categories' => 1,
+      'dfp_enable_ad_categories_bundles[tags]' => 'tags',
+    ];
+    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
+
+    // Create a new DFP Ad Category term.
+    $dfp_category = $this->createTerm($dfp_add_categories_vocabulary);
+    $control_tag = $this->createTerm($tags_vocabulary);
+    $test_tag = $this->createTerm($tags_vocabulary);
+
+    // Create a new "Article" node with these two tags.
+    $edit = [
+      'title' => $this->randomName(8),
+      'field_tags[und]' => $control_tag->name . ', ' . $test_tag->name,
+    ];
+    $this->drupalPost('node/add/article', $edit, t('Save'));
+
+    // Edit one the new tags and tag it with a DFP Ad Category.
+    $edit = [
+      'field_dfp_ad_categories[und]' => $dfp_category->tid,
+    ];
+    $this->drupalPost('taxonomy/term/' . $test_tag->tid . '/edit', $edit, t('Save'));
+
+    // Create a DFP ad slot that targets the [dfp_tag:ad_categories] token.
+    $edit = $this->dfpBasicTagEditValues();
+
+    // Create a tag that uses the slot token in a target.
+    $edit['slot'] = $this->randomName(8);
+    $edit['targeting[0][target]'] = 'categories';
+    $edit['targeting[0][value]'] = '[dfp_tag:ad_categories]';
+    $this->createTag($edit);
+
+    // Check that when we are on the node page, that the DFP Ad Category is used
+    // and that the control tag is also displayed.
+    $this->drupalGet('node/1');
+    $this->assertPropertySet('Targeting', 'categories', $control_tag->name);
+    $this->assertPropertySet('Targeting', 'categories', $dfp_category->name);
+
+    // Check that when we are on the term page for one of DFP Ad Categories that
+    // it is correctly included.
+    $this->drupalGet('taxonomy/term/' . $dfp_category->tid);
+    $this->assertPropertySet('Targeting', 'categories', $dfp_category->name);
+
+    // Check that when we are on a term page for a term with a coresponding
+    // DFP Ad Categories the DFP Ad Category is included.
+    $this->drupalGet('taxonomy/term/' . $test_tag->tid);
+    $this->assertPropertySet('Targeting', 'categories', $dfp_category->name);
+  }
+}
+
+class dfpAdTestTest extends dfpBaseTest {
+  /**
+   * Implements getInfo().
+   */
+  public static function getInfo() {
+    return [
+      'name' => t('Displaying AdTest DFP Tags'),
+      'description' => t('Tests for displaying overridden DFP tags for ad test purposes.'),
+      'group' => t('DFP'),
+    ];
+  }
+
+  function testTestPage() {
+    $tag = $this->createTag();
+
+    $override = $this->randomName(8);
+    $this->drupalGet('admin/structure/dfp_ads/test_page', ['query' => ['adunit_override' => $override]]);
+    $this->assertRaw('googletag.defineSlot("' . variable_get('dfp_network_id', '') . '/' . $override . '"', 'The Ad Unit Name was properly overridden on the Ad Test Page.');
+  }
+
+  function testQueryString() {
+    $tag = $this->createTag();
+
+    $override = $this->randomName(8);
+    variable_set('dfp_adtest_adunit', $override);
+    $this->drupalGet('admin/structure/dfp_ads/test_page', ['query' => ['adtest' => 'true']]);
+    $this->assertRaw('googletag.defineSlot("' . variable_get('dfp_network_id', '') . '/' . $override . '"', 'The Ad Unit Name was properly overridden using the adtest query string.');
+  }
+
+}
diff --git a/src/Token.php b/src/Token.php
new file mode 100644
index 0000000..801ae2b
--- /dev/null
+++ b/src/Token.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\Token.
+ */
+
+namespace Drupal\dfp;
+
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Utility\Token as CoreToken;
+use Drupal\dfp\View\TagView;
+
+/**
+ * A DFP token service to wrap core's service.
+ */
+class Token implements TokenInterface {
+
+  /**
+   * Drupal core's token service.
+   *
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $coreToken;
+
+  /**
+   * The route match service.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Token constructor.
+   *
+   * @param \Drupal\Core\Utility\Token $core_token
+   *   Drupal core's token service.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The route match service.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user.
+   */
+  public function __construct(CoreToken $core_token, RouteMatchInterface $route_match, AccountInterface $account) {
+    $this->coreToken = $core_token;
+    $this->routeMatch = $route_match;
+    $this->account = $account;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function replace($text, TagView $tag = NULL, array $options = [], BubbleableMetadata $bubbleable_metadata = NULL) {
+    $data = [
+      'user' => $this->account,
+    ];
+    if ($tag) {
+      $data['dfp_tag'] = $tag;
+    }
+
+    // Determine other data from the RouteMatch object.
+    $node = $this->routeMatch->getParameter('node');
+    if ($node) {
+      $data['node'] = $node;
+    }
+    $term = $this->routeMatch->getParameter('taxonomy_term');
+    if ($term) {
+      $data['term'] = $term;
+    }
+
+    return $this->coreToken->replace($text, $data, $options, $bubbleable_metadata);
+  }
+
+}
diff --git a/src/TokenInterface.php b/src/TokenInterface.php
new file mode 100644
index 0000000..ff40cc5
--- /dev/null
+++ b/src/TokenInterface.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\TokenInterface.
+ */
+
+namespace Drupal\dfp;
+
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\dfp\View\TagView;
+
+/**
+ * Interface for the DFP token service.
+ */
+interface TokenInterface {
+
+  /**
+   * Replaces all tokens in a given string with appropriate values.
+   *
+   * @param string $text
+   *   An HTML string containing replaceable tokens.
+   * @param \Drupal\dfp\View\TagView $tag
+   *   (optional) An TagView object that merges values of the Tag and global
+   *   settings. Defaults to NULL.
+   * @param array $options
+   *   (optional) A keyed array of settings and flags to control the token
+   *   replacement process.
+   * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
+   *   (optional) An object to which static::generate() and the hooks and
+   *   functions that it invokes will add their required bubbleable metadata.
+   *   Defaults to NULL.
+   *
+   * @return string
+   *   The token result is the entered HTML text with tokens replaced. The
+   *   caller is responsible for choosing the right escaping / sanitization. If
+   *   the result is intended to be used as plain text, using
+   *   PlainTextOutput::renderFromHtml() is recommended. If the result is just
+   *   printed as part of a template relying on Twig autoescaping is possible,
+   *   otherwise for example the result can be put into #markup, in which case
+   *   it would be sanitized by Xss::filterAdmin().
+   *
+   * @see \Drupal\Core\Utility\Token::replace()
+   */
+  public function replace($text, TagView $tag = NULL, array $options = [], BubbleableMetadata $bubbleable_metadata = NULL);
+
+}
diff --git a/src/View/TagList.php b/src/View/TagList.php
new file mode 100644
index 0000000..022033b
--- /dev/null
+++ b/src/View/TagList.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\View\TagList.
+ */
+
+namespace Drupal\dfp\View;
+
+use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Url;
+
+/**
+ * Defines a class to build a listing of DFP tag entities.
+ *
+ * @see \Drupal\dfp\View\TagList
+ */
+class TagList extends ConfigEntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['title'] = t('Ad Slot');
+    $header['size'] = [
+      'data' => t('Size(s)'),
+      'class' => [RESPONSIVE_PRIORITY_MEDIUM],
+    ];
+    $header['block'] = t('Block');
+
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /** @var \Drupal\dfp\Entity\TagInterface $entity */
+    $row['title']['data'] = $entity->label();
+    $row['size']['data'] = $entity->size();
+    $row['block']['data'] = $entity->hasBlock() ? $this->t('Yes') : $this->t('No');
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build = parent::render();
+    $build['table']['#empty'] = $this->t('No DFP tags available. <a href=":link">Add DFP tag</a>.',
+      [':link' => Url::fromRoute('entity.dfp_tag.add_form')->toString()]
+    );
+    return $build;
+  }
+
+}
diff --git a/src/View/TagView.php b/src/View/TagView.php
new file mode 100644
index 0000000..9b4cc10
--- /dev/null
+++ b/src/View/TagView.php
@@ -0,0 +1,397 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\View\TagView.
+ */
+
+namespace Drupal\dfp\View;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Url;
+use Drupal\dfp\Entity\TagInterface;
+use Drupal\dfp\TokenInterface;
+
+/**
+ * A value object to combine a DFP tag with global settings for display.
+ */
+class TagView {
+  use DependencySerializationTrait;
+
+  /**
+   * The short tag query string.
+   *
+   * @var string
+   */
+  protected $shortTagQueryString;
+
+  /**
+   * The ad unit with tokens replaced.
+   *
+   * @var string
+   */
+  protected $adUnit;
+
+  /**
+   * The targeting, altered and tokens replaced.
+   *
+   * @var array
+   */
+  protected $targeting;
+
+  /**
+   * The breakpoints.
+   *
+   * @var array
+   */
+  protected $breakpoints;
+
+  /**
+   * The click URL shared across object instances.
+   *
+   * @var string
+   */
+  protected static $clickUrl;
+
+  /**
+   * The global DFP configuration.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $globalSettings;
+
+  /**
+   * The DFP tag.
+   *
+   * @var \Drupal\dfp\Entity\TagInterface
+   */
+  protected $tag;
+
+  /**
+   * The DFP token service.
+   *
+   * @var \Drupal\dfp\TokenInterface
+   */
+  protected $token;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * TagView constructor.
+   *
+   * @param \Drupal\dfp\Entity\TagInterface $tag
+   *   The DFP tag.
+   * @param \Drupal\Core\Config\ImmutableConfig $global_settings
+   *   The DFP global configuration.
+   * @param \Drupal\dfp\TokenInterface $token
+   *   The DFP token service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(TagInterface $tag, ImmutableConfig $global_settings, TokenInterface $token, ModuleHandlerInterface $module_handler) {
+    $this->tag = $tag;
+    $this->globalSettings = $global_settings;
+    $this->token = $token;
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * The gets the slug.
+   *
+   * @return string
+   *   The slug.
+   */
+  public function getSlug() {
+    $slug = $this->tag->slug();
+    if (empty($slug)) {
+      $slug = $this->globalSettings->get('default_slug');
+    }
+    if ($slug == '<none>') {
+      $slug = "";
+    }
+    return $slug;
+  }
+
+  /**
+   * Gets the placeholder ID.
+   *
+   * @return string
+   *   The placeholder ID
+   */
+  public function getPlaceholderId() {
+    return 'js-dfp-tag-' . $this->tag->id();
+  }
+
+  /**
+   * Gets the ad unit.
+   *
+   * @return string
+   *   The ad unit.
+   */
+  public function getAdUnit() {
+    if (is_null($this->adUnit)) {
+      $adunit = $this->tag->adunit();
+      if (empty($adunit)) {
+        $adunit = $this->globalSettings->get('default_pattern');
+      }
+      $this->adUnit = '/' . $this->globalSettings->get('network_id') . '/' . $this->token->replace($adunit, $this, ['clear' => TRUE]);
+    }
+    return $this->adUnit;
+  }
+
+  /**
+   * Gets the raw size from the DFP tag.
+   *
+   * @return string
+   *   The ad size or sizes. Example: 300x600,300x250.
+   */
+  public function getRawSize() {
+    return $this->tag->size();
+  }
+
+  /**
+   * Gets the raw ad targeting from the DFP tag.
+   *
+   * @return array[]
+   *   Each value is a array containing two keys: 'target' and 'value'. Both
+   *   values are strings. Multiple value values are delimited by a comma.
+   */
+  public function getRawTargeting() {
+    return $this->tag->targeting();
+  }
+
+  /**
+   * Gets the ad slot.
+   *
+   * @return string
+   *   The ad slot. This is the same as the label for the configuration entity.
+   */
+  public function getSlot() {
+    return $this->tag->slot();
+  }
+
+  /**
+   * Gets the short tag query string.
+   *
+   * @return string
+   *   The short tag query string.
+   *
+   * @see https://support.google.com/dfp_sb/answer/2623168
+   */
+  public function getShortTagQueryString() {
+    if (is_null($this->shortTagQueryString)) {
+      // Build a key|values array and allow third party modules to modify it.
+      $key_values = [
+        'iu' => $this->getAdUnit(),
+        'sz' => str_replace(',', '|', $this->getRawSize()),
+        'c' => mt_rand(10000, 99999),
+      ];
+
+      $targets = [];
+      foreach ($this->getRawTargeting() as $data) {
+        $targets[] = $data['target'] . '=' . $data['value'];
+      }
+      if (!empty($targets)) {
+        $key_values['t'] = implode('&', $targets);
+      }
+      $this->moduleHandler->alter('dfp_short_tag_keyvals', $key_values);
+      $this->shortTagQueryString = UrlHelper::buildQuery($key_values);
+    }
+    return $this->shortTagQueryString;
+  }
+
+  /**
+   * Determines whether to display the tag as a short tag.
+   *
+   * @return bool
+   *   TRUE to display the tag as a short tag, FALSE if not.
+   */
+  public function isShortTag() {
+    return $this->tag->shortTag();
+  }
+
+  /**
+   * Gets the DFP ad tag identifier.
+   *
+   * @return string
+   *   The DFP ad tag identifier.
+   */
+  public function id() {
+    return $this->tag->id();
+  }
+
+  /**
+   * Gets the ad targeting.
+   *
+   * @return array[]
+   *   Each value is a array containing two keys: 'target' and 'value'. The
+   *   'target' value is a string and the 'value' value is an array of strings.
+   */
+  public function getTargeting() {
+    if (is_null($this->targeting)) {
+      $this->targeting = self::formatTargeting($this->tag->targeting(), $this->token, $this->moduleHandler, $this);
+    }
+    return $this->targeting;
+  }
+
+  /**
+   * Formats a targeting array.
+   *
+   * @param array $targeting
+   *   The targeting array. An array of arrays. Each each has two keys 'target'
+   *   and 'value'. The 'target' value is a string. The 'value' value is a
+   *   string with multiple values delimited by a comma.
+   * @param \Drupal\dfp\TokenInterface $token
+   *   The DFP token service.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\dfp\View\TagView|NULL $tag_view
+   *   (optional) The TagView object. Defaults to NULL.
+   *
+   * @return array
+   *   An array of arrays. Each each has two keys 'target' and 'value'. The
+   *   'target' value is a trimmed string. The 'value' value is an array of
+   *   strings, also trimmed.
+   */
+  public static function formatTargeting(array $targeting, TokenInterface $token, ModuleHandlerInterface $module_handler, TagView $tag_view = NULL) {
+    foreach ($targeting as $key => &$target) {
+      $target['target'] = trim($target['target']);
+      $target['value'] = $token->replace($target['value'], $tag_view, ['clear' => TRUE]);
+      // The target value could be blank if tokens are used. If so, remove it.
+      if (empty($target['value'])) {
+        unset($targeting[$key]);
+        continue;
+      }
+
+      // Allow other modules to alter the target.
+      $module_handler->alter('dfp_target', $target);
+
+      // Convert the values into an array.
+      $target['value'] = array_map('trim', explode(',', $target['value']));
+    }
+    return $targeting;
+  }
+
+  /**
+   * Gets the breakpoints.
+   *
+   * @return array[]
+   *   Each value is a array containing two keys: 'browser_size' and 'ad_sizes'.
+   *   The 'browser_size' is a value such as '[1024,768]'. The 'ad_sizes' value
+   *   contains a list of ad sizes to be be used at this 'browser_size' such as
+   *   '[[300,600],[300,250]]'.
+   *
+   * @see \Drupal\dfp\View\TagView::formatSize()
+   */
+  public function getBreakpoints() {
+    if (is_null($this->breakpoints)) {
+      $this->breakpoints = array_map(function ($breakpoint) {
+        return [
+          'browser_size' => self::formatSize($breakpoint['browser_size']),
+          'ad_sizes' => self::formatSize($breakpoint['ad_sizes']),
+        ];
+      }, $this->tag->breakpoints());
+    }
+    return $this->breakpoints;
+  }
+
+  /**
+   * Gets the ad size or sizes.
+   *
+   * @return string
+   *   The ad size or sizes. Example: 300x600,300x250.
+   */
+  public function getSize() {
+    return self::formatSize($this->tag->size());
+  }
+
+  /**
+   * Formats a size or sizes for javascript.
+   *
+   * @param string $size
+   *   A size to format. Multiple sizes delimited by comma. Example:
+   *   '300x600,300x250'.
+   *
+   * @return string
+   *   A string representing sizes that can be used in javascript. Example:
+   *   '[[300,600],[300,250]]'.
+   */
+  public static function formatSize($size) {
+    $formatted_sizes = [];
+
+    $sizes = explode(',', $size);
+    foreach ($sizes as $size) {
+      $formatted_size = explode('x', trim($size));
+      $formatted_sizes[] = '[' . implode(', ', $formatted_size) . ']';
+    }
+
+    return count($formatted_sizes) == 1 ? $formatted_sizes[0] : '[' . implode(', ', $formatted_sizes) . ']';
+  }
+
+  /**
+   * Gets the type of ads displayed when AdSense ads are used for backfill.
+   *
+   * @return string
+   *   The type of ads displayed when AdSense ads are used for backfill.
+   */
+  public function getAdsenseAdTypes() {
+    return $this->tag->adsenseAdTypes();
+  }
+
+  /**
+   * Gets the Adsense channel ID(s) when AdSense ads are used for backfill.
+   *
+   * @return string
+   *   The Adsense channel ID(s) when AdSense ads are used for backfill.
+   *   Multiple IDs are delimited by a + sign.
+   */
+  public function getAdsenseChannelIds() {
+    return $this->tag->adsenseChannelIds();
+  }
+
+  /**
+   * Gets the colors used when AdSense ads are used for backfill.
+   *
+   * @return string[]
+   *   An array keyed by setting with hex colors as values.
+   */
+  public function getAdSenseColors() {
+    return array_filter($this->tag->adsenseColors());
+  }
+
+  /**
+   * Gets the click URL.
+   *
+   * @return string
+   *   The click URL.
+   */
+  public function getClickUrl() {
+    // Since this can't change during a request statically cache it.
+    if (is_null(self::$clickUrl)) {
+      self::$clickUrl = (string) $this->globalSettings->get('click_url');
+      if (self::$clickUrl && !preg_match("/^https?:\/\//", self::$clickUrl)) {
+        self::$clickUrl = Url::fromUserInput(self::$clickUrl, ['absolute' => TRUE])->toString();
+      }
+    }
+    return self::$clickUrl;
+  }
+
+  /**
+   * Gets whether the slug should be hidden.
+   *
+   * @return bool
+   *   TRUE if the slug should be hidden, FALSE if not.
+   */
+  public function isSlugHidden() {
+    return $this->globalSettings->get('hide_slug');
+  }
+
+}
diff --git a/src/View/TagViewBuilder.php b/src/View/TagViewBuilder.php
new file mode 100644
index 0000000..2ccfbfd
--- /dev/null
+++ b/src/View/TagViewBuilder.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\dfp\View\TagViewBuilder.
+ */
+
+namespace Drupal\dfp\View;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityViewBuilder;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\dfp\Entity\TagInterface;
+use Drupal\dfp\TokenInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a DFP Tag view builder.
+ */
+class TagViewBuilder extends EntityViewBuilder {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * DFP token service.
+   *
+   * @var \Drupal\dfp\TokenInterface
+   */
+  protected $token;
+
+  /**
+   * Constructs a new BlockViewBuilder.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager service.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\dfp\TokenInterface $token
+   *   DFP token service.
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, TokenInterface $token) {
+    parent::__construct($entity_type, $entity_manager, $language_manager);
+    $this->moduleHandler = $module_handler;
+    $this->configFactory = $config_factory;
+    $this->token = $token;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager'),
+      $container->get('language_manager'),
+      $container->get('module_handler'),
+      $container->get('config.factory'),
+      $container->get('dfp.token')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildComponents(array &$build, array $entities, array $displays, $view_mode) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
+    $build = $this->viewMultiple([$entity], $view_mode, $langcode);
+    return reset($build);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) {
+    /** @var \Drupal\dfp\Entity\TagInterface[] $entities */
+    $build = [];
+    foreach ($entities as $tag) {
+      // @todo Ensure a tag is only once on the page.
+      // @todo Get cache-ability based on tokens used in TagView...
+      $global_settings = $this->configFactory->get('dfp.settings');
+      $tag_view = new TagView($tag, $global_settings, $this->token, $this->moduleHandler());
+
+      $tag_id = $tag->id();
+      $build[$tag_id] = [
+        '#cache' => [
+          'keys' => ['entity_view', 'dfp_tag', $tag_id],
+        ],
+      ];
+
+      // Sort out the cache tags and contexts.
+      $cacheable_metadata = CacheableMetadata::createFromObject($global_settings);
+      $cacheable_metadata->merge(CacheableMetadata::createFromObject($tag));
+      $cacheable_metadata->addCacheTags($this->getCacheTags());
+      $cacheable_metadata->applyTo($build[$tag_id]);
+
+      $build[$tag_id] += static::buildPreTag($tag_view);
+
+    }
+
+    return $build;
+  }
+
+  /**
+   * Builds a #pre_render-able DFP tag render array.
+   *
+   * @param \Drupal\dfp\View\TagView $tag_view
+   *   A DFP tag.
+   *
+   * @return array
+   *   A render array with a #pre_render callback to render the DFP tag.
+   *
+   * @see \Drupal\dfp\DfpResponseAttachmentsProcessor::processAttachments()
+   */
+  protected static function buildPreTag(TagView $tag_view) {
+    $build = [
+      '#contextual_links' => [
+        'dfp_tag' => [
+          'route_parameters' => ['dfp_tag' => $tag_view->id()],
+        ],
+      ],
+    ];
+    if ($tag_view->isShortTag()) {
+      $build['tag'] = [
+        '#theme' => 'dfp_short_tag',
+        '#url_jump' => 'http://' . TagInterface::GOOGLE_SHORT_TAG_SERVICES_URL . '/jump?' . $tag_view->getShortTagQueryString(),
+        '#url_ad' => 'http://' . TagInterface::GOOGLE_SHORT_TAG_SERVICES_URL . '/ad?' . $tag_view->getShortTagQueryString(),
+      ];
+    }
+    else {
+      $build['tag'] = [
+        '#theme' => 'dfp_tag',
+      ];
+      // Attach a tag. These attachments are processed by
+      // \Drupal\dfp\DfpResponseAttachmentsProcessor::processAttachments().
+      $build['#attached']['dfp_slot'][] = $tag_view;
+    }
+    $build['tag']['#tag'] = $tag_view;
+
+    return $build;
+  }
+
+}
diff --git a/dfp.adtest.inc b/src/View/dfp.adtest.inc
similarity index 79%
rename from dfp.adtest.inc
rename to src/View/dfp.adtest.inc
index 1f18544..40c8a9b 100644
--- a/dfp.adtest.inc
+++ b/src/View/dfp.adtest.inc
@@ -9,33 +9,33 @@
  * Page callback for the DFP test page. It displays all tags on the site.
  */
 function dfp_adtest_page() {
-  $output = array();
+  $output = [];
   $form = drupal_get_form('dfp_adtest_form');
 
-  $output[] = array(
+  $output[] = [
     '#markup' => render($form),
-    '#attached' => array(
-      'css' => array(
+    '#attached' => [
+      'css' => [
         'dfp-admin' => drupal_get_path('module', 'dfp') . '/dfp.admin.css',
-      ),
-    ),
-  );
+      ],
+    ],
+  ];
 
   $tags = dfp_tag_load_all();
   foreach ($tags as $tag) {
-    $tag_slot = array(
-      'tag slot name' => array(
+    $tag_slot = [
+      'tag slot name' => [
         '#markup' => '<h3>' . check_plain($tag->slot) . '</h3>',
-      ),
-    );
+      ],
+    ];
     $tag_tag = dfp_tag($tag->machinename);
-    $tag_devel = array();
+    $tag_devel = [];
     if (module_exists('devel')) {
-      $tag_devel['object'] = array(
+      $tag_devel['object'] = [
         '#markup' => kpr($tag, TRUE),
-      );
+      ];
     }
-    $output[] = array($tag_slot, $tag_tag, $tag_devel);
+    $output[] = [$tag_slot, $tag_tag, $tag_devel];
   }
 
   return $output;
@@ -49,24 +49,24 @@ function dfp_adtest_form($form, &$form_state) {
   form_load_include($form_state, 'inc', 'dfp', 'dfp.adtest');
 
   $form['#method'] = 'get';
-  $form['adunit_override'] = array(
+  $form['adunit_override'] = [
     '#type' => 'textfield',
     '#title' => t('Ad Unit Override'),
     '#default_value' => isset($_GET['adunit_override']) ? $_GET['adunit_override'] : '',
     '#description' => t('Override the Ad Unit value for all the ad tags below. Use the tokens below to define how the ad unit should display. Example: [dfp_tag:network_id]/test/[dfp_tag:slot]'),
-  );
-  $form['tokens'] = array(
+  ];
+  $form['tokens'] = [
     '#theme' => 'token_tree',
-    '#token_types' => array('dfp_tag', 'node', 'term', 'user'),
+    '#token_types' => ['dfp_tag', 'node', 'term', 'user'],
     '#global_types' => TRUE,
     '#click_insert' => TRUE,
     '#dialog' => TRUE,
     '#weight' => 100,
-  );
-  $form['submit'] = array(
+  ];
+  $form['submit'] = [
     '#type' => 'submit',
     '#value' => t('Override'),
-  );
+  ];
 
   return $form;
 }
diff --git a/templates/dfp-js-head-bottom.html.twig b/templates/dfp-js-head-bottom.html.twig
new file mode 100644
index 0000000..c90cd76
--- /dev/null
+++ b/templates/dfp-js-head-bottom.html.twig
@@ -0,0 +1,45 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a DFP short tag.
+ *
+ * Available variables:
+ * - async_rendering
+ * - single_request
+ * - collapse_empty_divs
+ * - disable_init_load
+ * - targeting
+ *
+ * @see dfp_page_attachments()
+ *
+ * @ingroup themeable
+ */
+#}
+<script type="text/javascript">
+googletag.cmd.push(function() {
+  {% if async_rendering %}
+    googletag.pubads().enableAsyncRendering();
+  {% else %}
+    googletag.pubads().enableSyncRendering();
+  {% endif %}
+  {% if single_request %}
+    googletag.pubads().enableSingleRequest();
+  {% endif %}
+  {% if collapse_empty_divs == 1 %}
+    googletag.pubads().collapseEmptyDivs();
+  {% elseif collapse_empty_divs == 2 %}
+    googletag.pubads().collapseEmptyDivs(true);
+  {% endif %}
+  {% if disable_init_load %}
+    googletag.pubads().disableInitialLoad();
+  {% endif %}
+  {% for target in targeting %}
+    googletag.pubads().setTargeting('{{ target.target }}', [
+      {%- for value in target.value -%}
+      '{{ value }}'{%- if not loop.last -%},{%- endif -%}
+      {%- endfor -%}
+    ]);
+  {% endfor %}
+});
+googletag.enableServices();
+</script>
diff --git a/templates/dfp-js-head-top.html.twig b/templates/dfp-js-head-top.html.twig
new file mode 100644
index 0000000..e4d1fa8
--- /dev/null
+++ b/templates/dfp-js-head-top.html.twig
@@ -0,0 +1,25 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a DFP short tag.
+ *
+ * Available variables:
+ * - google_tag_services_url:
+ *
+ * @see dfp_page_attachments()
+ *
+ * @ingroup themeable
+ */
+#}
+<script type="text/javascript">
+// Initialize the google variables.
+var googletag = googletag || {};
+googletag.cmd = googletag.cmd || [];
+// Add a place to store the slot name variable.
+googletag.slots = googletag.slots || {};
+(function() {
+  var useSSL = "https:" == document.location.protocol;
+  var src = (useSSL ? 'https:' : 'http:') + '//{{ google_tag_services_url }}';
+  document.write('<scr' + 'ipt src="' + src + '"></scr' + 'ipt>');
+})();
+</script>
diff --git a/templates/dfp-short-tag.html.twig b/templates/dfp-short-tag.html.twig
new file mode 100644
index 0000000..58a555f
--- /dev/null
+++ b/templates/dfp-short-tag.html.twig
@@ -0,0 +1,18 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a DFP short tag.
+ *
+ * Available variables:
+ * - tag: The DFP Tag configuration entity.
+ * - url_jump: Url to link to.
+ * - url_ad: Url to display the ad.
+ *
+ * @see \Drupal\dfp\View\Tag\TagViewBuilder::buildPreTag()
+ *
+ * @ingroup themeable
+ */
+#}
+<a href="{{ url_jump }}">
+  <img src="{{ url_ad }}">
+</a>
diff --git a/templates/dfp-slot-definition-js.html.twig b/templates/dfp-slot-definition-js.html.twig
new file mode 100644
index 0000000..1056a9e
--- /dev/null
+++ b/templates/dfp-slot-definition-js.html.twig
@@ -0,0 +1,55 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a DFP short tag.
+ *
+ * Available variables:
+ * - tag: TagView object for a DFP tag.
+ *
+ * @see \Drupal\dfp\View\Tag\TagViewBuilder::buildPreTag()
+ *
+ * @ingroup themeable
+ */
+#}
+<script type="text/javascript">
+  {% if tag.breakpoints %}
+      // Start by defining breakpoints for this ad.
+      var mapping = googletag.sizeMapping()
+      {% for breakpoint in tag.breakpoints %}
+        .addSize({{ breakpoint.browser_size }}, {{ breakpoint.ad_sizes }})
+      {% endfor %}
+      .build();
+  {% endif %}
+
+  googletag.slots["{{ tag.id }}"] = googletag.defineSlot("{{ tag.adUnit }}", {{ tag.size }}, "{{ tag.placeholderId }}")
+
+  {% if tag.clickUrl %}
+    .setClickUrl("{{ tag.clickUrl }}")
+  {% endif %}
+  .addService(googletag.pubads())
+
+  {% if tag.adsenseAdTypes %}
+    .set('adsense_ad_types', '{{ tag.adsenseAdTypes }}')
+  {% endif %}
+
+  {% if tag.adsenseChannelIds %}
+    .set('adsense_channel_ids', '{{ tag.adsenseChannelIds }}')
+  {% endif %}
+
+  {% for key, color in tag.adSenseColors %}
+    .set('adsense_{{ key }}_color', '#{{ color|upper }}')
+  {% endfor %}
+
+  {% for target in tag.targeting %}
+    .setTargeting('{{ target.target }}', [
+      {%- for value in target.value -%}
+        '{{ value }}'{%- if not loop.last -%},{%- endif -%}
+      {%- endfor -%}
+    ])
+  {% endfor %}
+  {% if tag.breakpoints %}
+    // Apply size mapping when there are breakpoints.
+    .defineSizeMapping(mapping)
+  {% endif %}
+  ;
+</script>
diff --git a/templates/dfp-tag.html.twig b/templates/dfp-tag.html.twig
new file mode 100644
index 0000000..597b2f2
--- /dev/null
+++ b/templates/dfp-tag.html.twig
@@ -0,0 +1,25 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a DFP tag.
+ *
+ * Available variables:
+ * - tag: \Drupal\dfp\View\TagView object.
+ *
+ * @see \Drupal\dfp\View\Tag\TagViewBuilder::buildPreTag()
+ *
+ * @ingroup themeable
+ */
+#}
+<div id="{{ tag.placeholderId }}">
+    {% if tag.isSlugHidden == false and tag.slug %}
+        <div>
+            {{ tag.slug }}
+        </div>
+    {% endif %}
+    <script type="text/javascript">
+        googletag.cmd.push(function() {
+            googletag.display('{{ tag.placeholderId }}');
+        });
+    </script>
+</div>
diff --git a/tests/dfp.test b/tests/dfp.test
deleted file mode 100644
index 48480fa..0000000
--- a/tests/dfp.test
+++ /dev/null
@@ -1,549 +0,0 @@
-<?php
-
-/**
- * @file
- * Test file for DFP module.
- */
-
-class dfpBaseTest extends DrupalWebTestCase {
-  /**
-   * Implementation of setUp().
-   */
-  function setUp() {
-    // Enable a couple modules.
-    parent::setUp('ctools', 'dfp', 'taxonomy');
-    menu_rebuild();
-
-    // Create an admin user with all the permissions needed to run tests.
-    $this->admin_user = $this->drupalCreateUser(array('administer DFP', 'administer taxonomy', 'access administration pages', 'administer blocks', 'bypass node access', 'administer nodes', 'edit terms in 1'));
-    $this->drupalLogin($this->admin_user);
-
-    // Add the some global settings needed for dfp ads to work.
-    variable_set('dfp_network_id', '12345');
-    variable_set('dfp_default_slug', $this->randomName(8));
-  }
-
-  /**
-   * Create a simple $edit array to be used on the DFP tag form to create a new
-   * DFP tag object.
-   */
-  function dfpBasicTagEditValues() {
-    $machinename = drupal_strtolower($this->randomName(8));
-    $basic_tag = array(
-      'machinename' => $machinename,
-      'slot' => $machinename,
-      'size' => implode(',', $this->dfpGenerateSize(2)),
-      'adunit' => $this->randomName(8),
-      'block' => 1,
-      'settings[slug]' => $this->randomName(32),
-      'settings[adsense_ad_types]' => '',
-      'settings[adsense_channel_ids]' => '',
-      'settings[adsense_colors][background]' => '',
-      'settings[adsense_colors][border]' => '',
-      'settings[adsense_colors][link]' => '',
-      'settings[adsense_colors][text]' => '',
-      'settings[adsense_colors][url]' => '',
-      'targeting[0][target]' => $this->randomName(8),
-      'targeting[0][value]' => $this->randomName(8),
-      'breakpoints[0][browser_size]' => $this->dfpGenerateSize(),
-      'breakpoints[0][ad_sizes]' => implode(',', $this->dfpGenerateSize(2)),
-    );
-
-    return $basic_tag;
-  }
-
-  /**
-   * Generate a random size (or array or sizes) to use when testing tags
-   * formatted as ###x###.
-   */
-  function dfpGenerateSize($count = 1) {
-    $sizes = array('300x250', '300x600', '728x90', '728x10', '160x600', '120x80', '300x100', '50x50', '160x300');
-    shuffle($sizes);
-
-    return $count == 1 ? array_pop($sizes) : array_slice($sizes, 0, min($count, count($sizes)));
-  }
-
-  /**
-   * Creates a basic dfp ad tag.
-   */
-  function createTag($edit = array()) {
-    // Create a new tag.
-    $edit += $this->dfpBasicTagEditValues();
-    $this->drupalPost('admin/structure/dfp_ads/add', $edit, t('Save'));
-
-    // Load the tag object.
-    $tag = dfp_tag_load($edit['machinename']);
-    $this->assertTrue(is_object($tag) && $tag->machinename = $edit['machinename'], 'The new tag was saved correctly.');
-
-    // Display the new tag.
-    $edit = array();
-    $delta = 'dfp_' . (drupal_strlen($tag->machinename) >= 32 ? md5($tag->machinename) : $tag->machinename);
-    $edit['blocks[' . $delta . '][region]'] = 'sidebar_first';
-    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
-
-    return $tag;
-  }
-
-  /**
-   * Edit a given tag specified by $machinename with the given values.
-   */
-  function editTag($machinename, &$edit) {
-    // Make sure there is no machinename set when we are editing.
-    if (isset($edit['machinename'])) {
-      unset($edit['machinename']);
-    }
-    $this->drupalPost('admin/structure/dfp_ads/list/' . $machinename . '/edit', $edit, t('Save'));
-    $tag = dfp_tag_load($machinename);
-    return $tag;
-  }
-
-  /**
-   * Create a taxonomy term.
-   */
-  function createTerm($vocabulary) {
-    $term = new stdClass();
-    $term->name = $this->randomName();
-    $term->description = $this->randomName();
-    // Use the first available text format.
-    $term->format = db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField();
-    $term->vid = $vocabulary->vid;
-    taxonomy_term_save($term);
-    return $term;
-  }
-
-  /**
-   * Assert that a property is properly being set.
-   */
-  function assertPropertySet($property, $key, $val) {
-    $pattern = '|' . '.set' . $property . '\("' . $key . '",{1}\s(.)*' . addslashes($val) . '|';
-    $this->assertPattern($pattern, 'A ' . $property . ' property was set for ' . $key . ' = ' . $val);
-  }
-
-  /**
-   * Assert that a property is not being set.
-   */
-  function assertPropertyNotSet($property, $key, $val) {
-    $pattern = '|' . '.set' . $property . '\("' . $key . '",{1}\s(.)*' . addslashes($val) . '|';
-    $this->assertNoPattern($pattern, 'A ' . $property . ' property was not set for ' . $key . ' = ' . $val);
-  }
-
-}
-
-class dfpDisplayTagTest extends dfpBaseTest {
-  /**
-   * Implements getInfo().
-   */
-  public static function getInfo() {
-    return array(
-      'name' => t('Displaying DFP Tags'),
-      'description' => t('Tests for displaying DFP tags.'),
-      'group' => t('DFP'),
-    );
-  }
-
-  function testDisplayTag() {
-    // Create a simple tag as a block.
-    $tag = $this->createTag();
-    $this->assertRaw('googletag.defineSlot("' . variable_get('dfp_network_id', '') . '/' . $tag->adunit . '", ' . dfp_format_size($tag->size) . ', "' . $tag->placeholder_id . '")', 'The ad slot has been correctly defined in javascript.');
-
-    // Create a tag with a machine name longer than 32 characters.
-    $edit = array('machinename' => drupal_strtolower($this->randomName(64)));
-    $tag = $this->createTag($edit);
-    $this->assertRaw('googletag.defineSlot("' . variable_get('dfp_network_id', '') . '/' . $tag->adunit . '", ' . dfp_format_size($tag->size) . ', "' . $tag->placeholder_id . '")', 'The ad slot with a very long machine name has been correctly defined in javascript.');
-  }
-
-  function testDisplayTagWithMapping() {
-    $edit = $this->dfpBasicTagEditValues();
-
-    // Create a simple tag with a mapping. Verify javascript on page.
-    $tag = $this->createTag($edit);
-    $mapping_sizes = explode(',', $edit['breakpoints[0][ad_sizes]']);
-    $size_count = count($mapping_sizes);
-    // Calculate addSize mappings.
-    $mapping_tag = '.addSize(';
-    $mapping_tag .= '[' . str_replace('x', ', ', $edit['breakpoints[0][browser_size]']) . '], ';
-    $mapping_tag .= ($size_count > 1) ? '[' : '';
-    for ($i = 0; $i < $size_count; $i++) {
-      $mapping_sizes[$i] = '[' . str_replace('x', ', ', $mapping_sizes[$i]) . ']';
-      $mapping_tag .= ($i + 1 !== $size_count) ? $mapping_sizes[$i] . ', ' : $mapping_sizes[$i];
-    }
-    $mapping_tag .= ($size_count > 1) ? '])' : ')';
-    $this->assertRaw('googletag.sizeMapping()', 'The ad slot correctly attaches size mapping.');
-    $this->assertRaw('.defineSizeMapping(mapping)', 'The ad slot correctly defines size mapping.');
-    $this->assertRaw($mapping_tag, 'The ad slot correctly defines specific size mappings.');
-
-    // Create a tag with invalid browser size mappings.
-    $edit['breakpoints[0][browser_size]'] = $this->randomName(8);
-    $this->editTag($tag->machinename, $edit);
-    $this->assertText(t('The browser size can only contain numbers and the character x.'), 'An error was correctly thrown when invalid characters.');
-
-    // Create a tag with invalid ad size mappings.
-    $edit['breakpoints[0][browser_size]'] = $this->dfpGenerateSize();
-    $edit['breakpoints[0][ad_sizes]'] = $this->randomName(8);
-    $this->editTag($tag->machinename, $edit);
-    $this->assertText(t('The ad size(s) can only contain numbers, the character x and commas.'), 'An error was correctly thrown when invalid characters.');
-  }
-
-  function testSlug() {
-    $edit = $this->dfpBasicTagEditValues();
-
-    // Create a tag without a slug, display it and ensure the default slug is
-    // displayed.
-    variable_set('dfp_default_slug', $this->randomName(8));
-    $edit['settings[slug]'] = '';
-    $tag = $this->createTag($edit);
-    $this->assertRaw('<div class="slug">' . dfp_format_slug(variable_get('dfp_default_slug', '')) . '</div>', 'The default slug is correctly used when no slug exists for an individual tag.');
-
-    // Change the slug to <none> and ensure that no slug is displayed.
-    $edit['settings[slug]'] = '<none>';
-    $this->editTag($tag->machinename, $edit);
-    $this->assertNoRaw('<div class="slug">', t('No slug is appearing when "@none" is used.', array('@none' => '<none>')));
-
-    // Specify a slug and check that it shows instead of the default slug.
-    $slug = $this->randomName(8);
-    $edit['settings[slug]'] = $slug;
-    $this->editTag($tag->machinename, $edit);
-    $this->assertRaw('<div class="slug">' . $slug . '</div>', 'The provided slug is correctly displayed with its tag.');
-  }
-
-  function testTargeting() {
-    $edit = $this->dfpBasicTagEditValues();
-
-    // Create a tag with a target with only one value.
-    $tag = $this->createTag($edit);
-    $this->assertPropertySet('Targeting', $edit['targeting[0][target]'], $edit['targeting[0][value]']);
-
-    // Create a tag with a target with multiple values.
-    $values = array($this->randomName(8), $this->randomName(8), $this->randomName(8));
-    $edit['targeting[0][target]'] = $this->randomName(8);
-    $edit['targeting[0][value]'] = implode(', ', $values);
-    $this->editTag($tag->machinename, $edit);
-    $this->assertPropertySet('Targeting', $edit['targeting[0][target]'], implode('","', $values));
-
-    // Create a tag with a target but no value.
-    $edit['targeting[0][target]'] = $this->randomName(8);
-    $edit['targeting[0][value]'] = '';
-    $this->editTag($tag->machinename, $edit);
-    $this->assertText(t('The value cannot be empty if a target exists.'), 'An error was correctly thrown when a target has no value.');
-
-    // Create a tag with an empty target, but a value.
-    $edit['targeting[0][target]'] = '';
-    $edit['targeting[0][value]'] = $this->randomName(8);
-    $this->editTag($tag->machinename, $edit);
-    $this->assertText(t('The target cannot be empty if a value exists.'), 'An error was correctly thrown when a target is blank, but a value has been entered.');
-
-    // Create a tag with multiple targets.
-    $count = 3;
-    for ($i = 0; $i < $count; $i++) {
-      $edit['targeting[' . $i . '][target]'] = $this->randomName(8);
-      $edit['targeting[' . $i . '][value]'] = $this->randomName(8);
-      $this->drupalPost('admin/structure/dfp_ads/list/' . $tag->machinename . '/edit', $edit, t('Add another target'));
-      $this->editTag($tag->machinename, $edit);
-    }
-
-    for ($i = 0; $i < $count; $i++) {
-      $this->assertPropertySet('Targeting', $edit['targeting[' . $i . '][target]'], $edit['targeting[' . $i . '][value]']);
-    }
-    // Make sure that empty values are ignored.
-    $this->drupalPost('admin/structure/dfp_ads/list/' . $tag->machinename . '/edit', $edit, t('Add another target'));
-    $this->editTag($tag->machinename, $edit);
-    $this->assertPropertyNotSet('Targeting', '', '');
-
-    // Test that target can be removed.
-    $edit['targeting[0][target]'] = '';
-    $edit['targeting[0][value]'] = '';
-    $this->editTag($tag->machinename, $edit);
-    $this->assertPropertyNotSet('Targeting', '', '');
-  }
-
-  function testBackfill() {
-    $edit = $this->dfpBasicTagEditValues();
-
-    // Create a tag with backfill settings.
-    $colors = array('background', 'border', 'link', 'text', 'url');
-    $types = array('text', 'image', 'text_image');
-    shuffle($types);
-
-    $edit['settings[adsense_ad_types]'] = array_pop($types);
-    $edit['settings[adsense_channel_ids]'] = $this->randomName(8);
-    foreach ($colors as $color) {
-      $edit['settings[adsense_colors][' . $color . ']'] = drupal_strtoupper($this->randomName(8));
-    }
-    $tag = $this->createTag($edit);
-    $this->assertPropertySet('', 'adsense_ad_types', $edit['settings[adsense_ad_types]']);
-    $this->assertPropertySet('', 'adsense_channel_ids', $edit['settings[adsense_channel_ids]']);
-    foreach ($colors as $color) {
-      $this->assertPropertySet('', 'adsense_' . $color . '_color', $edit['settings[adsense_colors][' . $color . ']']);
-    }
-  }
-
-}
-
-class dfpAdCategoriesTest extends dfpBaseTest {
-  /**
-   * Implements getInfo().
-   */
-  public static function getInfo() {
-    return array(
-      'name' => t('DFP Ad Categories'),
-      'description' => t('Tests the functionaility of DFP ad categories.'),
-      'group' => t('DFP'),
-    );
-  }
-
-  function testEnableDisableAdCategories() {
-    // Check that enabling DFP Ad Categories for the "tags" vocabulary works.
-    $edit = array(
-      'dfp_enable_ad_categories' => 1,
-      'dfp_enable_ad_categories_bundles[tags]' => 'tags',
-    );
-    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
-    $this->drupalGet('admin/structure/taxonomy/tags/add');
-    $this->assertFieldByName('field_dfp_ad_categories[und]');
-
-    // Check that disabling DFP Ad Categories vocabulary works even when a
-    // bundle is selected.
-    $edit = array(
-      'dfp_enable_ad_categories' => 0,
-    );
-    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
-    $this->drupalGet('admin/structure/taxonomy/tags/add');
-    $this->assertNoFieldByName('field_dfp_ad_categories[und]');
-
-    // Check that disabling DFP Ad Categories on a specific vocabulary works.
-    // For this test to be valid, we first need to reenable DFP Ad Categories.
-    $edit = array(
-      'dfp_enable_ad_categories' => 1,
-      'dfp_enable_ad_categories_bundles[tags]' => 'tags',
-    );
-    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
-    $this->drupalGet('admin/structure/taxonomy/tags/add');
-    $edit = array(
-      'dfp_enable_ad_categories_bundles[tags]' => 0,
-    );
-    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
-    $this->drupalGet('admin/structure/taxonomy/tags/add');
-    $this->assertNoFieldByName('field_dfp_ad_categories[und]');
-  }
-
-  // Test that DFP Catetories are displayed properly.
-  function testDisplayAdCategories() {
-    $dfp_add_categories_vocabulary = taxonomy_vocabulary_machine_name_load('dfp_ad_categories');
-    $tags_vocabulary = taxonomy_vocabulary_machine_name_load('tags');
-
-    // Turn on DFP ad categories for the "tags" vocabulary.
-    $edit = array(
-      'dfp_enable_ad_categories' => 1,
-      'dfp_enable_ad_categories_bundles[tags]' => 'tags',
-    );
-    $this->drupalPost('admin/structure/dfp_ads/settings', $edit, t('Save configuration'));
-
-    // Create a new DFP Ad Category term.
-    $dfp_category = $this->createTerm($dfp_add_categories_vocabulary);
-    $control_tag = $this->createTerm($tags_vocabulary);
-    $test_tag = $this->createTerm($tags_vocabulary);
-
-    // Create a new "Article" node with these two tags.
-    $edit = array(
-      'title' => $this->randomName(8),
-      'field_tags[und]' => $control_tag->name . ', ' . $test_tag->name,
-    );
-    $this->drupalPost('node/add/article', $edit, t('Save'));
-
-    // Edit one the new tags and tag it with a DFP Ad Category.
-    $edit = array(
-      'field_dfp_ad_categories[und]' => $dfp_category->tid,
-    );
-    $this->drupalPost('taxonomy/term/' . $test_tag->tid . '/edit', $edit, t('Save'));
-
-    // Create a DFP ad slot that targets the [dfp_tag:ad_categories] token.
-    $edit = $this->dfpBasicTagEditValues();
-
-    // Create a tag that uses the slot token in a target.
-    $edit['slot'] = $this->randomName(8);
-    $edit['targeting[0][target]'] = 'categories';
-    $edit['targeting[0][value]'] = '[dfp_tag:ad_categories]';
-    $tag = $this->createTag($edit);
-
-    // Check that when we are on the node page, that the DFP Ad Category is used
-    // and that the control tag is also displayed.
-    $this->drupalGet('node/1');
-    $this->assertPropertySet('Targeting', 'categories', $control_tag->name);
-    $this->assertPropertySet('Targeting', 'categories', $dfp_category->name);
-
-    // Check that when we are on the term page for one of DFP Ad Categories that
-    // it is correctly included.
-    $this->drupalGet('taxonomy/term/' . $dfp_category->tid);
-    $this->assertPropertySet('Targeting', 'categories', $dfp_category->name);
-
-    // Check that when we are on a term page for a term with a coresponding
-    // DFP Ad Categories the DFP Ad Category is included.
-    $this->drupalGet('taxonomy/term/' . $test_tag->tid);
-    $this->assertPropertySet('Targeting', 'categories', $dfp_category->name);
-  }
-}
-
-class dfpTokensTest extends dfpBaseTest {
-  /**
-   * Implements getInfo().
-   */
-  public static function getInfo() {
-    return array(
-      'name' => t('DFP Tokens'),
-      'description' => t('Tests for custom tokens created by the DFP module.'),
-      'group' => t('DFP'),
-    );
-  }
-
-  function testSlotToken() {
-    $edit = $this->dfpBasicTagEditValues();
-
-    // Create a tag that uses the slot token in a target.
-    $test_slot = $this->randomName(8);
-    $edit['slot'] = $test_slot;
-    $edit['targeting[0][target]'] = 'slot';
-    $edit['targeting[0][value]'] = '[dfp_tag:slot]';
-    $tag = $this->createTag($edit);
-    $this->assertPropertySet('Targeting', 'slot', $test_slot);
-  }
-
-  function testNetworkIDToken() {
-    $edit = $this->dfpBasicTagEditValues();
-
-    // Create a tag that uses the networkid token in a target.
-    $test_network_id = $this->randomName(8);
-    variable_set('dfp_network_id', $test_network_id);
-    $edit['targeting[0][target]'] = 'network id';
-    $edit['targeting[0][value]'] = '[dfp_tag:network_id]';
-    $tag = $this->createTag($edit);
-    $this->assertPropertySet('Targeting', 'network id', $test_network_id);
-  }
-
-  function testUrlPartsToken() {
-    // Create a tag that uses the url parts token in a target.
-    $tag = $this->createTag();
-    $url = 'admin/structure/dfp_ads';
-    $url_parts = explode('/', $url);
-
-    $max_parts_to_test = 3;
-    for ($i = 1; $i <= $max_parts_to_test; $i++) {
-      $edit['targeting[0][target]'] = 'url parts';
-      $edit['targeting[0][value]'] = '[dfp_tag:url_parts:' . $i . ']';
-      $this->editTag($tag->machinename, $edit);
-
-      $this->drupalGet($url);
-      $replacement = implode('/', array_slice($url_parts, 0, $i));
-      $this->assertPropertySet('Targeting', 'url parts', $replacement);
-    }
-  }
-
-}
-
-class dfpGlobalsTest extends dfpBaseTest {
-  /**
-   * Implements getInfo().
-   */
-  public static function getInfo() {
-    return array(
-      'name' => t('Gloabl DFP Tag Settings'),
-      'description' => t('Tests for the settings defined in admin/structure/dfp_ads/settings.'),
-      'group' => t('DFP'),
-    );
-  }
-
-  function testGlobalSettingsOn() {
-    $injected_js = $this->randomName(32);
-    $target = array(
-      'target' => $this->randomName(8),
-      'value' => $this->randomName(8),
-    );
-
-    variable_set('dfp_async_rendering', '1');
-    variable_set('dfp_single_request', '1');
-    variable_set('dfp_collapse_empty_divs', '1');
-    variable_set('dfp_injected_js', $injected_js);
-    variable_set('dfp_targeting', array($target));
-    $this->drupalGet('/');
-    $this->assertRaw('googletag.pubads().enableAsyncRendering();', 'Asyncronous rendering is turned on.');
-    $this->assertRaw('googletag.pubads().enableSingleRequest();', 'Single request is turned on.');
-    $this->assertRaw('googletag.pubads().collapseEmptyDivs();', 'Collapse empty divs is turned on.');
-    $this->assertRaw($injected_js, 'Injected javascript correctly appears on the page.');
-    $this->assertRaw('googletag.pubads().setTargeting("' . $target['target'] . '", "' . $target['value'] . '");', 'Global targetting values appear correclty in javascript.');
-  }
-
-  function testGlobalSettingsOff() {
-    variable_set('dfp_async_rendering', '0');
-    variable_set('dfp_single_request', '0');
-    variable_set('dfp_collapse_empty_divs', '0');
-    $this->drupalGet('/');
-    $this->assertNoRaw('googletag.pubads().enableAsyncRendering();', 'Asyncronous rendering is turned off.');
-    $this->assertNoRaw('googletag.pubads().enableSingleRequest();', 'Single request is turned off.');
-    $this->assertNoRaw('googletag.pubads().collapseEmptyDivs();', 'Collapse empty divs is turned off.');
-  }
-
-}
-
-class dfpAdTestTest extends dfpBaseTest {
-  /**
-   * Implements getInfo().
-   */
-  public static function getInfo() {
-    return array(
-      'name' => t('Displaying AdTest DFP Tags'),
-      'description' => t('Tests for displaying overridden DFP tags for ad test purposes.'),
-      'group' => t('DFP'),
-    );
-  }
-
-  function testTestPage() {
-    $tag = $this->createTag();
-
-    $override = $this->randomName(8);
-    $this->drupalGet('admin/structure/dfp_ads/test_page', array('query' => array('adunit_override' => $override)));
-    $this->assertRaw('googletag.defineSlot("' . variable_get('dfp_network_id', '') . '/' . $override . '"', 'The Ad Unit Name was properly overridden on the Ad Test Page.');
-  }
-
-  function testQueryString() {
-    $tag = $this->createTag();
-
-    $override = $this->randomName(8);
-    variable_set('dfp_adtest_adunit', $override);
-    $this->drupalGet('admin/structure/dfp_ads/test_page', array('query' => array('adtest' => 'true')));
-    $this->assertRaw('googletag.defineSlot("' . variable_get('dfp_network_id', '') . '/' . $override . '"', 'The Ad Unit Name was properly overridden using the adtest query string.');
-  }
-
-}
-
-class DFPUnitTest extends DrupalUnitTestCase {
-  protected $profile = 'testing';
-
-  /**
-   * Implementation of setUp().
-   */
-  function setUp() {
-    // Enable a couple modules.
-    drupal_load('module', 'dfp');
-    parent::setUp();
-  }
-
-  /**
-   * Implements getInfo().
-   */
-  public static function getInfo() {
-    return array(
-      'name' => t('DFP Unit Tests'),
-      'description' => t('Checks the integrity of certain DFP functions.'),
-      'group' => t('DFP'),
-    );
-  }
-
-  function testDFPformatSize() {
-    $sizes = '300x250';
-    $expected_result = '[300, 250]';
-
-    $this->assertTrue(dfp_format_size($sizes) == $expected_result, 'The dfp_format_sizes function correctly handles an single size.');
-    $sizes = '300x250,728x90';
-    $expected_result = '[[300, 250], [728, 90]]';
-    $this->assertTrue(dfp_format_size($sizes) == $expected_result, 'The dfp_format_sizes function correctly handles multiple sizes.');
-  }
-}
diff --git a/tests/src/Unit/DfpHtmlResponseAttachmentsProcessorTest.php b/tests/src/Unit/DfpHtmlResponseAttachmentsProcessorTest.php
new file mode 100644
index 0000000..51d53d8
--- /dev/null
+++ b/tests/src/Unit/DfpHtmlResponseAttachmentsProcessorTest.php
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\dfp\Unit\DfpHtmlResponseAttachmentsProcessorTest.
+ */
+
+namespace Drupal\Tests\dfp\Unit;
+
+use Drupal\Core\Asset\AssetCollectionRendererInterface;
+use Drupal\Core\Asset\AssetResolverInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\dfp\DfpHtmlResponseAttachmentsProcessor;
+use Drupal\dfp\TokenInterface;
+use Drupal\dfp\View\TagView;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * @coversDefaultClass \Drupal\dfp\DfpHtmlResponseAttachmentsProcessor
+ * @group dfp
+ */
+class DfpHtmlResponseAttachmentsProcessorTest extends UnitTestCase {
+
+  /**
+   * A mock core html attachment processor.
+   *
+   * @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $attachmentProcessor;
+
+  /**
+   * A mock DFP token service.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $token;
+
+  /**
+   * A mock asset resolver service.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $assetResolver;
+
+  /**
+   * A mock CSS collection renderer.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $cssCollectionRenderer;
+
+  /**
+   * A mock JS collection renderer.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $jsCollectionRenderer;
+
+  /**
+   * A mock RequestStack.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $requestStack;
+
+  /**
+   * A mock renderer.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $renderer;
+
+  /**
+   * A mock module handler.
+   *
+   * @var \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected $moduleHandler;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Mock core attachment processor.
+    $this->attachmentProcessor = $this->getMockBuilder(AttachmentsResponseProcessorInterface::class)->disableOriginalConstructor()->getMock();
+    $this->attachmentProcessor->method('processAttachments')
+      ->willReturnArgument(0);
+
+    $this->token = $this->prophesize(TokenInterface::class);
+    $this->assetResolver = $this->prophesize(AssetResolverInterface::class);
+    $this->cssCollectionRenderer = $this->prophesize(AssetCollectionRendererInterface::class);
+    $this->jsCollectionRenderer = $this->prophesize(AssetCollectionRendererInterface::class);
+    $this->requestStack = $this->prophesize(RequestStack::class);
+    $this->renderer = $this->prophesize(RendererInterface::class);
+    $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
+  }
+
+  /**
+   * @covers ::processAttachments
+   */
+  public function testProcessAttachments() {
+    // Create a response with two dfp_slot attachments and ensure that the
+    // they are converted to html_head attachments. Also ensure that the other
+    // html_head elements are added to create the necessary javascript in the
+    // right order.
+    $response = new HtmlResponse();
+    for ($i = 1; $i < 3; $i++) {
+      $tag = $this->prophesize(TagView::class);
+      $tag->id()->willReturn($i);
+      $attachments['dfp_slot'][] = $tag->reveal();
+    }
+    $response->setAttachments($attachments);
+    $config_factory = $this->getConfigFactoryStub(['dfp.settings' => ['targeting' => []]]);
+
+    $response = $this->getDfpAttachmentProcessor($config_factory)->processAttachments($response);
+    $this->assertEquals('dfp-js-head-top', $response->getAttachments()['html_head'][0][1]);
+    $this->assertEquals('dfp-slot-definition-1', $response->getAttachments()['html_head'][1][1]);
+    $this->assertEquals('dfp-slot-definition-2', $response->getAttachments()['html_head'][2][1]);
+    $this->assertEquals('dfp-js-head-bottom', $response->getAttachments()['html_head'][3][1]);
+    $this->assertArrayNotHasKey('dfp_slot', $response->getAttachments(), 'The dfp_slot attachments are converted to html_head attachments.');
+  }
+
+  /**
+   * @covers ::processAttachments
+   */
+  public function testProcessAttachmentsNoSlots() {
+    // Ensure that if there are no slots nothing is added to the attachments.
+    $response = new HtmlResponse();
+    $config_factory = $this->getConfigFactoryStub();
+    $response = $this->getDfpAttachmentProcessor($config_factory)->processAttachments($response);
+    $this->assertEmpty($response->getAttachments());
+  }
+
+  /**
+   * Creates a DfpHtmlResponseAttachmentsProcessor object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   A mock config factory that can contain 'dfp.settings' configuration.
+   *
+   * @return \Drupal\dfp\DfpHtmlResponseAttachmentsProcessor
+   *   The DfpHtmlResponseAttachmentsProcessor object.
+   */
+  protected function getDfpAttachmentProcessor(ConfigFactoryInterface $config_factory) {
+    return new DfpHtmlResponseAttachmentsProcessor(
+      $this->attachmentProcessor,
+      $this->token->reveal(),
+      $this->assetResolver->reveal(),
+      $config_factory,
+      $this->cssCollectionRenderer->reveal(),
+      $this->jsCollectionRenderer->reveal(),
+      $this->requestStack->reveal(),
+      $this->renderer->reveal(),
+      $this->moduleHandler->reveal()
+    );
+  }
+
+}
diff --git a/tests/src/Unit/View/TagViewTest.php b/tests/src/Unit/View/TagViewTest.php
new file mode 100644
index 0000000..95f644f
--- /dev/null
+++ b/tests/src/Unit/View/TagViewTest.php
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\dfp\Unit\View\TagViewTest.
+ */
+
+namespace Drupal\Tests\dfp\Unit\View;
+
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\dfp\TokenInterface;
+use Drupal\dfp\Entity\TagInterface;
+use Drupal\dfp\View\TagView;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\dfp\View\TagView
+ * @group dfp
+ */
+class TagViewTest extends UnitTestCase {
+
+  /**
+   * @covers ::formatSize
+   * @dataProvider formatSizeProvider
+   */
+  public function testFormatSize($size, $expected) {
+    $this->assertSame($expected, TagView::formatSize($size));
+  }
+
+  /**
+   * Data provider for self::testFormatSize().
+   */
+  public function formatSizeProvider() {
+    return [
+      ['300x250 ', '[300, 250]'],
+      ['300x250, 728x90 ', '[[300, 250], [728, 90]]'],
+    ];
+  }
+
+  /**
+   * @covers ::getSlug
+   * @dataProvider getSlugProvider
+   */
+  public function testGetSlug($tag_slug, $default_slug, $expected_slug) {
+    $tag = $this->prophesize(TagInterface::class);
+    $tag->slug()->willReturn($tag_slug);
+    $config_factory = $this->getConfigFactoryStub(['dfp.settings' => ['default_slug' => $default_slug]]);
+    $token = $this->prophesize(TokenInterface::class)->reveal();
+    $module_handler = $this->prophesize(ModuleHandlerInterface::class)->reveal();
+    $tag_view = new TagView($tag->reveal(), $config_factory->get('dfp.settings'), $token, $module_handler);
+    $this->assertSame($expected_slug, $tag_view->getSlug());
+  }
+
+  /**
+   * Data provider for self::testGetSlug().
+   */
+  public function getSlugProvider() {
+    return [
+      ['slug', 'default_slug', 'slug'],
+      ['', 'default_slug', 'default_slug'],
+      ['<none>', 'default_slug', ''],
+    ];
+  }
+
+  /**
+   * @covers ::getAdUnit
+   * @dataProvider getAdUnitProvider
+   */
+  public function testGetAdUnit($tag_ad_unit, $default_ad_unit, $network_id, $expected_adunit) {
+    $tag = $this->prophesize(TagInterface::class);
+    $tag->adunit()->willReturn($tag_ad_unit);
+    $config_factory = $this->getConfigFactoryStub(['dfp.settings' => ['default_pattern' => $default_ad_unit, 'network_id' => $network_id]]);
+    $token = $this->getMock(TokenInterface::class);
+    $token->method('replace')->willReturnArgument(0);
+    $module_handler = $this->prophesize(ModuleHandlerInterface::class)->reveal();
+    $tag_view = new TagView($tag->reveal(), $config_factory->get('dfp.settings'), $token, $module_handler);
+    $this->assertSame($expected_adunit, $tag_view->getAdUnit());
+  }
+
+  /**
+   * Data provider for self::testGetAdUnit().
+   */
+  public function getAdUnitProvider() {
+    return [
+      ['adunit', 'default_adunit', '12345', '/12345/adunit'],
+      ['', 'default_adunit', '67890', '/67890/default_adunit'],
+    ];
+  }
+
+  /**
+   * @covers ::getShortTagQueryString
+   * @dataProvider getShortTagQueryStringProvider
+   */
+  public function testGetShortTagQueryString($tag_ad_unit, $tag_sizes, $tag_targeting, $network_id, $regex) {
+    $tag = $this->prophesize(TagInterface::class);
+    $tag->adunit()->willReturn($tag_ad_unit);
+    $tag->size()->willReturn($tag_sizes);
+    $tag->targeting()->willReturn($tag_targeting);
+    $config_factory = $this->getConfigFactoryStub(['dfp.settings' => ['default_pattern' => 'default_adunit', 'network_id' => $network_id]]);
+    $token = $this->getMock(TokenInterface::class);
+    $token->method('replace')->willReturnArgument(0);
+    $module_handler = $this->prophesize(ModuleHandlerInterface::class)->reveal();
+    $tag_view = new TagView($tag->reveal(), $config_factory->get('dfp.settings'), $token, $module_handler);
+    $this->assertRegExp($regex, $tag_view->getShortTagQueryString());
+  }
+
+  /**
+   * Data provider for self::testGetShortTagQueryString().
+   */
+  public function getShortTagQueryStringProvider() {
+    return [
+      ['adunit', '300x200', [], '12345', '|^iu=/12345/adunit&sz=300x200&c=[0-9]{5}$|'],
+      ['adunit', '300x200', [['target' => 'target', 'value' => 'value,value2']], '12345', '|^iu=/12345/adunit&sz=300x200&c=[0-9]{5}&t=target%3Dvalue%2Cvalue2$|'],
+      ['adunit', '300x200', [['target' => 'target', 'value' => 'value,value2'], ['target' => 'target2', 'value' => 'value3']], '12345', '|^iu=/12345/adunit&sz=300x200&c=[0-9]{5}&t=target%3Dvalue%2Cvalue2%26target2%3Dvalue3$|'],
+    ];
+  }
+
+}
diff --git a/theme/dfp_short_tag.tpl.php b/theme/dfp_short_tag.tpl.php
deleted file mode 100644
index 2ed724e..0000000
--- a/theme/dfp_short_tag.tpl.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-  /**
-   * @file
-   * Default template for dfp short tags.
-   */
-?>
-
-<a href="<?php print $url_jump; ?>">
-  <img src="<?php print $url_ad; ?>">
-</a>
diff --git a/theme/dfp_tag.tpl.php b/theme/dfp_tag.tpl.php
deleted file mode 100644
index 0dd89d7..0000000
--- a/theme/dfp_tag.tpl.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-  /**
-   * @file
-   * Default template for dfp tags.
-   */
-?>
-
-<div <?php print drupal_attributes($placeholder_attributes) ?>>
-  <?php if (isset($slug)):
-    print drupal_render($slug);
-  endif; ?>
-  <script type="text/javascript">
-    googletag.cmd.push(function() {
-      googletag.display("<?php print $tag->placeholder_id ?>");
-    });
-  </script>
-</div>
