GTM Data Layer: Preventing Stale Data During SPA Navigation
Google Tag Manager's data layer is a cumulative key-value store. Every call to dataLayer.push() deep-merges the pushed object into GTM's internal model - it never removes existing keys on its own. A key is only updated when a push explicitly includes it.
Example:
// Navigation to Article A
dataLayer.push({
purple: {
view_event_name: "article_view",
parameters: {
article_id: "article-a",
publish_date: "2024-01-10",
author: "Jane Doe"
}
}
})
// Navigation to Article B (author not provided this time)
dataLayer.push({
purple: {
view_event_name: "article_view",
parameters: {
article_id: "article-b",
publish_date: "2024-03-22"
// author is missing - Jane Doe is still in GTM's model
}
}
})After the second push, GTM's internal model for purple.parameters.author still contains "Jane Doe" from Article A.
SPA mode vs. non-SPA mode
In SPA mode (the default Purple storefront behavior), the JavaScript environment persists for the entire session. The dataLayer array and GTM's internal model are never reset - every push accumulates on top of the previous state.
In non-SPA mode (where each navigation causes a full-page reload), the JavaScript environment is destroyed and recreated on each page. dataLayer is re-initialized as an empty array, so stale data from previous pages is impossible. If your storefront is configured in non-SPA mode, this guide does not apply.
The Purple behavior
When the Purple storefront fires a view event via the GTM tracking plugin, it pushes to window.dataLayer under the purple namespace. The parameters included in that push are the parameters listed in tracking_config.json for that particular view/event.
For example, if view A defines author as a parameter but view B does not, navigating from A to B leaves the author value from view A in GTM's model. Any GTM tag that reads purple.parameters.author will see stale data.
This is most visible with article/content views where fields like publish date, author, taxonomy, or image URLs are defined inconsistently across different view configurations.
Use explicit Parameters in tracking_config.json
List every parameter you want to track in the parameters section of every affected view, even if the value will not always be available.
When a placeholder like {{AUTHOR}} cannot be resolved (because the current page has no author data), the storefront pushes an empty string "" for that key. This overwrites the stale value from the previous view with an empty string, which is the correct behavior - GTM reads no author for the current view.
In tracking_config.json, add the full set of content-related parameters to every view that could follow a content view during SPA navigation:
{
"google_tag_manager": {
"viewsEnabledByDefault": true,
"views": {
"STOREFRONT_HOME": {
"templates": {
"name": "storefront_home"
},
"parameters": {
"view": "{{VIEW}}",
"content_id": "{{CONTENT_ID}}",
"content_name": "{{CONTENT_NAME}}",
"issue_id": "{{ISSUE_ID}}",
"issue_name": "{{ISSUE_NAME}}",
"publication_id": "{{PUBLICATION_ID}}",
"publication_name": "{{PUBLICATION_NAME}}"
}
},
"ISSUE_CONTENT": {
"templates": {
"name": "issue_content"
},
"parameters": {
"view": "{{VIEW}}",
"content_id": "{{CONTENT_ID}}",
"content_name": "{{CONTENT_NAME}}",
"issue_id": "{{ISSUE_ID}}",
"issue_name": "{{ISSUE_NAME}}",
"publication_id": "{{PUBLICATION_ID}}",
"publication_name": "{{PUBLICATION_NAME}}"
}
}
}
}
}The key rule: if a parameter appears in any view, it should appear in all views that can be reached from it via SPA navigation without a full page reload.
For project-specific fields (stored as custom issue properties), the placeholder follows the pattern {{ISSUE_PROPERTY_<KEY>}}. For example, a custom property publish-date would be {{ISSUE_PROPERTY_PUBLISH_DATE}}.
Available Standard Placeholders
These placeholders are resolved by the storefront at the time the view/event fires:
Content / Article
- {{CONTENT_ID}} - ID of the current content item
- {{CONTENT_NAME}} - Name/title of the current content item
Issue
- {{ISSUE_ID}}, {{ISSUE_NAME}}, {{ISSUE_PRODUCT_ID}}
- {{ISSUE_PRICE}}, {{ISSUE_PRICE_VALUE}}, {{ISSUE_PRICE_CURRENCY}}
- {{ISSUE_PURCHASABLE}}, {{ISSUE_PURCHASED}}, {{ISSUE_LATEST}}
- {{ISSUE_PROPERTY_<KEY>}} - Any custom issue property
Publication
- {{PUBLICATION_ID}}, {{PUBLICATION_NAME}}, {{PUBLICATION_TYPE}}
- {{PUBLICATION_PROPERTY_<KEY>}} - Any custom publication property
Navigation
- {{VIEW}} - The current storefront view name
- {{POPUP}} - Whether the view is open as a popup
Page (readmode)
- {{PAGE_ID}}, {{PAGE_INDEX}}, {{PAGE_NUMBER}}, {{PAGE_LABEL}}, {{PAGE_TITLE}}, {{PAGE_ALIAS}}, {{PAGE_SECTION}}
Subscription / Purchase
- {{SUBSCRIPTION_ID}}, {{SUBSCRIPTION_NAME}}, {{SUBSCRIPTION_TYPE}}
- {{PRODUCT_ID}}, {{PRODUCT_NAME}}, {{PRODUCT_PRICE}}
- {{TRANSACTION_ID}}, {{CURRENCY_CODE}}
Verification
GTM Preview Mode (Tag Assistant)
- Open the storefront in Chrome with GTM Preview Mode active
- Navigate to a content view (Article A)
- Inspect the data layer in Tag Assistant - note all purple.parameters values
- Navigate to a different content view (Article B) without reloading
- Check purple.parameters again - all fields should reflect Article B only, with empty strings for any fields not applicable to Article B
Experience Logs
The Purple storefront has a built-in tracking debugger. Enable it in the browser console while in preview mode:
window.purple.tracking.debug({ logging: true })Or persistently via localStorage (survives page refresh):
localStorage.setItem('tracking.debug.logging', 'true')
// then reload the pageOnce enabled, every tracked event is printed to the console:
[TRACK] [VIEW] ISSUE_CONTENT
Parameters:
content_id = article-b,
content_name = My Article B,
issue_id = issue-456,
publish_date = ,
author = {{AUTHOR}}Fields that resolve to their placeholder (because no value is available for the current view) show as empty. This confirms they are being explicitly pushed and will overwrite stale values in GTM's model.
Navigate between two articles and verify that all listed parameters reflect the current article on each view event and that fields not applicable to the current article appear as empty strings rather than retaining values from the previous article.