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
Method 1: Form Include in Page Template (Recommended)
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.