Textpattern Analytics Implementation Guide | OpsBlu Docs

Textpattern Analytics Implementation Guide

Install tracking scripts, build data layers, and debug analytics on Textpattern using txp tags, page templates, forms, plugins, and variable tags.

Analytics Architecture on Textpattern

Textpattern (Txp) uses a tag-based templating system where pages, forms, and styles combine to render output. Analytics implementation leverages several platform components:

  • Page templates define the overall HTML structure for each section, with <txp:output_form> calls to include reusable markup
  • Forms are reusable template fragments (like partials) referenced from page templates and article forms
  • Txp tags provide access to article fields, site metadata, section info, and conditional logic directly in templates
  • Plugins extend Textpattern with PHP-based functionality, including custom tags and event hooks
  • Variable tags (<txp:variable>) store and retrieve values within a template render cycle for data layer construction
  • Admin preferences contain site-wide settings accessible via <txp:pref>

Textpattern renders pages by selecting a page template based on the URL's section, then populating it with article data and form includes.


Installing Tracking Scripts

Create a form called analytics_head containing your tracking scripts:

<!-- Form: analytics_head (type: misc) -->

<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXX');
</script>

Include it in your page template before </head>:

<!-- Page template: default -->

<!DOCTYPE html>
<html lang="<txp:lang />">
<head>
  <title><txp:page_title /></title>
  <txp:output_form form="analytics_head" />
</head>
<body>
  <txp:output_form form="header" />

  <main>
    <txp:article />
  </main>

  <txp:output_form form="footer" />
</body>
</html>

Every section that uses this page template automatically gets the tracking code.

Method 2: Section-Conditional Loading

Use Textpattern's conditional tags to load different tracking code per section:

<!-- In the page template -->

<txp:if_section name="blog,articles">
  <txp:output_form form="analytics_content" />
<txp:else />
  <txp:output_form form="analytics_default" />
</txp:if_section>

Or skip analytics on specific sections:

<txp:if_section name="admin,preview">
<txp:else />
  <txp:output_form form="analytics_head" />
</txp:if_section>

Method 3: Plugin-Based Injection

For plugins that need to inject scripts via PHP, register a callback on the textpattern event:

<?php
// Plugin: abc_analytics

register_callback('abc_analytics_inject', 'textpattern');

function abc_analytics_inject() {
    global $pretext;

    // Skip on admin pages
    if (txpinterface !== 'public') return;

    $propertyId = get_pref('abc_analytics_property_id', 'G-XXXXXXX');

    $script = <<<EOT
<script async src="https://www.googletagmanager.com/gtag/js?id={$propertyId}"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', '{$propertyId}');
</script>
EOT;

    // Inject before </head>
    ob_start(function($buffer) use ($script) {
        return str_replace('</head>', $script . '</head>', $buffer);
    });
}

Store the property ID in admin preferences:

<?php
// In plugin install callback
set_pref('abc_analytics_property_id', 'G-XXXXXXX', 'abc_analytics', PREF_PLUGIN, 'text_input');

Data Layer Implementation

Using Variable Tags

Textpattern's <txp:variable> tag lets you build up data values through the template render, then output them together:

<!-- In the page template, before the data layer script -->

<txp:variable name="dl_section" value='<txp:section />' />
<txp:variable name="dl_page_title" value='<txp:page_title />' />
<txp:variable name="dl_site_url" value='<txp:site_url />' />

<txp:if_individual_article>
  <txp:article limit="1" form="data_layer_article" />
<txp:else />
  <txp:variable name="dl_page_type" value="listing" />
</txp:if_individual_article>

Create a form data_layer_article to capture article-specific fields:

<!-- Form: data_layer_article (type: article) -->

<txp:variable name="dl_page_type" value="article" />
<txp:variable name="dl_article_id" value='<txp:article_id />' />
<txp:variable name="dl_article_title" value='<txp:title />' />
<txp:variable name="dl_author" value='<txp:author_email />' />
<txp:variable name="dl_category1" value='<txp:category1 title="1" />' />
<txp:variable name="dl_category2" value='<txp:category2 title="1" />' />
<txp:variable name="dl_posted" value='<txp:posted format="%Y-%m-%d" />' />
<txp:variable name="dl_modified" value='<txp:modified format="%Y-%m-%d" />' />
<txp:variable name="dl_keywords" value='<txp:keywords />' />

Then output the data layer in the page template:

<!-- In the page template, inside <head> -->

<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'page_type': '<txp:variable name="dl_page_type" />',
  'section': '<txp:variable name="dl_section" />',
  'page_title': '<txp:variable name="dl_page_title" />',
  <txp:if_individual_article>
  'article_id': '<txp:variable name="dl_article_id" />',
  'article_title': '<txp:variable name="dl_article_title" />',
  'author': '<txp:variable name="dl_author" />',
  'category_1': '<txp:variable name="dl_category1" />',
  'category_2': '<txp:variable name="dl_category2" />',
  'published': '<txp:variable name="dl_posted" />',
  'modified': '<txp:variable name="dl_modified" />',
  'keywords': '<txp:variable name="dl_keywords" />',
  </txp:if_individual_article>
  'site_url': '<txp:variable name="dl_site_url" />'
});
</script>

Custom Field Data

Textpattern custom fields are accessed via <txp:custom_field>:

<txp:variable name="dl_product_sku" value='<txp:custom_field name="product_sku" />' />
<txp:variable name="dl_product_price" value='<txp:custom_field name="product_price" />' />

Category and Section Context

<txp:if_category>
  <txp:variable name="dl_page_type" value="category" />
  <txp:variable name="dl_category_name" value='<txp:category title="1" />' />
  <txp:variable name="dl_category_type" value='<txp:category type="1" />' />
</txp:if_category>

<txp:if_search>
  <txp:variable name="dl_page_type" value="search_results" />
  <txp:variable name="dl_search_query" value='<txp:search_term />' />
</txp:if_search>

Common Issues

Tag Parsing Order

Textpattern parses tags from the inside out. When nesting tags for data layer construction, the inner tag resolves first:

<!-- This works: inner tag resolves, then variable stores result -->
<txp:variable name="dl_title" value='<txp:title />' />

<!-- This breaks if title contains quotes -->
<txp:variable name="dl_title" value='<txp:title />' />

For article titles containing single quotes or special characters, the variable value can break the JavaScript string. Textpattern does not have a built-in JSON escape function. Use a plugin like smd_wrap or escape in the JavaScript output:

<script>
var rawTitle = document.createElement('div');
rawTitle.textContent = '<txp:title />';
window.dataLayer.push({'article_title': rawTitle.textContent});
</script>

Messy vs Clean URL Modes

Textpattern supports both messy URLs (?s=section&id=42) and clean URLs (/section/article-title). Your data layer should use <txp:permlink /> for canonical article URLs rather than the raw request path to ensure consistency regardless of URL mode.

Caching and Dynamic Content

Textpattern caches article content but re-renders page templates on each request. Data layer values derived from Txp tags render fresh each time. However, if you use a page-level cache plugin (like aks_cache), all data layer output gets cached.

For cached setups, move user-specific data layer values to client-side JavaScript that fetches from an uncached endpoint.

Article List Pages vs Individual Articles

The same page template handles both article list pages and individual article views. Use <txp:if_individual_article> to differentiate:

<txp:if_individual_article>
  <!-- Single article: full data layer with article fields -->
<txp:else />
  <!-- List page: section-level data layer only -->
</txp:if_individual_article>

Without this check, article-specific tags like <txp:title /> output nothing on list pages, creating empty data layer values.


Platform-Specific Considerations

Two Category Slots: Textpattern articles have exactly two category fields (category1 and category2). Unlike systems with unlimited taxonomy, your data layer is limited to these two unless you use custom fields or a plugin to add more categorization.

Section = URL Path: In Textpattern, sections map directly to URL segments (/blog/, /products/). The section name is both a routing mechanism and a content organizer. Use <txp:section /> as a reliable page-level dimension in your data layer.

No Built-in User System (Public): Textpattern does not have a public-facing user login by default. There is no user state to track on the frontend unless you add a membership plugin. Data layer user fields are only relevant if you've extended Textpattern with a membership/login plugin.

Plugin Tag Registration: Textpattern plugins can register custom tags that become available in templates. If you write an analytics plugin, register a tag like <txp:abc_datalayer /> that editors can place anywhere in templates:

<?php
Txp::get('\Textpattern\Tag\Registry')->register('abc_datalayer');

function abc_datalayer($atts) {
    global $thisarticle;
    $data = [
        'article_id' => $thisarticle['thisid'] ?? null,
        'section' => $thisarticle['section'] ?? '',
    ];
    return '<script>window.dataLayer.push(' . json_encode($data) . ');</script>';
}

Flat File Structure: Textpattern stores all templates, forms, and pages in the database (not the file system by default). Changes to analytics forms are made in the Textpattern admin under Presentation > Forms. Some developers use the ied_plugin_composer or file-based template plugins to manage templates in version control.

Admin Preferences for Config: Store analytics configuration (property IDs, enable/disable flags) in Textpattern's preference system rather than hardcoding in templates. Access via <txp:pref name="analytics_property_id" /> in templates or get_pref('analytics_property_id') in PHP plugins.