Use this to put Google Analytics (GA4) and the Meta (Facebook) Pixel on an Appmint-hosted site, and to claim the site in Google Search Console and Bing Webmaster Tools. There are two IDs, set once for the site and optionally replaced on one page, plus two verification codes. Appmint writes the tags into every page for you.
The tags fire for every visitor, before any cookie consent. Appmint does not hold GA or the Pixel back until a visitor accepts cookies. In a test, a fresh visitor got the _ga cookie and a Pixel PageView on first load, with a consent banner still on screen. If the site needs consent first (visitors in the EU, UK and similar), use step 5, and make the consent banner with Site notice: banner, popup, age gate or cookie consent.
For cookies of your own (UTM source, a referral code), see Site cookies from the address, with consent.
Ask first
- Which site? Tracking is set per site.
- The GA4 measurement ID. Ask for it, don't guess. It's in Google Analytics › Admin › Data streams › the web stream, and looks like
G-ABC123XYZ. A Universal AnalyticsUA-…, Google AdsAW-…or Google tagGT-…ID also works the same way. - The Pixel ID. It's in Meta Events Manager › Data sources, a number like
123456789012345. - The whole site, or one page? One page can use its own IDs (a campaign landing page, for example). Everywhere else keeps the site's IDs.
- Does it need cookie consent first? If yes, use step 5 instead of the plain fields for the ID that has to wait.
- Verification codes. For Search Console use the HTML tag method, and for Bing use the meta tag method. You need only the
contentvalue, e.g.abc123XYZ…, not the whole<meta>tag.
Steps
1. Site Config: the IDs for the whole site
In Studio, open /app/welcome › Site Config › Tracking & Social, or open the site switcher in Build Studio's header › Site Config. Fill in Google Analytics and Pixel, then Save changes. They are stored on the site record under data.tracking (websitemint site-config-form.tsx 938–948).
| Field (Studio label) | Stored as | What goes in | What the site does with it |
|---|---|---|---|
| Google Analytics | tracking.googleAnalytic | G-ABC123XYZ, or another Google tag ID | Loads https://www.googletagmanager.com/gtag/js?id=<ID> and runs gtag('js', new Date()); gtag('config', '<ID>') |
| Pixel | tracking.pixel | the Pixel ID | Runs Meta's base code (loads fbevents.js), then fbq('init', '<ID>'); fbq('track', 'PageView') |
| Google site verification | tracking.googleSiteVerification | the content value only | Step 2 |
| Bing site verification | tracking.bingSiteVerification | the content value only | Step 2 |
| Cookies | tracking.cookies | see Site cookies |
How the IDs are cleaned (base-app app/[[...slug]]/page-assets.ts 89–110, every row tested):
| Typed | Used as | Why |
|---|---|---|
G-TEST123 | G-TEST123 | already right |
test123 | G-TEST123 | no known prefix, so G- is added and the ID is upper-cased |
g-partial1 | G-PARTIAL1 | upper-cased |
ua-12345-1 | UA-12345-1 | known prefix (G, UA, AW, GT, GTM, DC), upper-cased |
G-ABC | G-ABC | spaces around it are trimmed |
12 34<x>, or anything with a character outside A–Z a–z 0–9 _ -, or over 64 characters | nothing | dropped without a word. No tag at all is written for that field |
The Pixel ID is only trimmed and checked against the same character rule. It isn't changed.
For the AI: read the site with repo_find_one on site, then write repo_update on site with patch: { "tracking": { …the whole existing tracking object…, "googleAnalytic": "G-…", "pixel": "…" } }. repo_update merges only one level deep, so a tracking object without cookies or the verification codes deletes them. Resend everything that was there (appengine repository.skill.ts 171–178).
Hands on: the IDs as stored.
Check: repo_find_one on the site shows data.tracking.googleAnalytic and data.tracking.pixel, and nothing else under tracking was lost.
Stop: these tags go live on every page of the site the moment the site is saved, and they send each visitor's page views to Google and Meta. Show the person the IDs and which site, and wait for a yes. If they need consent first, do step 5 instead.
2. Site Config: Google and Bing verification
In the same Tracking & Social card, fill in Google site verification and Bing site verification. The site writes them as <meta name="google-site-verification" content="…"> and <meta name="msvalidate.01" content="…"> in the head of its pages (base-app meta.tsx 202–210). Then press Verify in Search Console or Bing.
- These are site-level only. A page can't carry its own verification codes.
- Paste only the
contentvalue. A whole<meta …>tag is written into the tag'scontentattribute as text, and the check fails. - They must be under
tracking. base-app's own copy of the site model also declaresseo.googleSiteVerificationandseo.bingSiteVerification(lib/models/site.ts625–632), but nothing reads those. Studio's form and its Classic form both writetracking.*, which is the right place.
Hands on: nothing new.
Check: open the home page's source (view-source:https://<site host>/) and find both meta tags. Then press Verify in Search Console and Bing.
3. Build Studio: one page with its own IDs (optional)
A page can carry its own tracking.googleAnalytic, tracking.pixel and tracking.cookies. Each field is chosen separately: a page with only its own googleAnalytic still uses the site's Pixel. An empty or invalid page value falls back to the site's (page-assets.ts 122–123).
Don't set a page's tracking in Build Studio. Page settings › Page Config shows a Tracking group, but it saves the value under a key literally named tracking.googleAnalytic instead of inside tracking (websitemint build-studio-property.tsx 126, with the field path from form-render.tsx 139). The site never reads that key. Tested: a page saved this way, by create or by a full update, rendered with no GA tag and no Pixel. Set it through the API instead.
Write it one of these ways (both tested):
repo_updateonpagewithpatch: { "tracking": { "googleAnalytic": "G-LANDING1", "pixel": "…", "cookies": [ …any it already has… ] } }. Again, send the whole object.- Raw HTTP:
POST /repository/update-partial/page/<sk>with dot-paths, e.g.{ "data.tracking.googleAnalytic": "G-LANDING1" }. This changes only that field. Never send{ "data": { … } }, which replaces the page's wholedata.
Where a page's own tracking counts:
| URL | Whose tracking is used |
|---|---|
| An ordinary page | the page's own fields, then the site's |
A URL a Site Feature owns (/store, /blog, /book, …) | the page the feature borrows as its frame, not a stub page at that URL (base-app app/[[...slug]]/layout.tsx 58–68) |
/login, /register, /account/…, /checkout/…, /order/… | the site's, plus the My Account layout page's (or account-layout's) own. With neither layout page, none load there (lib/resolve-layout-page.ts 45–65) |
| A 404 | the site's |
A parent page's or root's own tracking is not passed down to the pages inside it. Only the page itself counts.
Hands on: the page's id and its IDs.
Check: repo_find_one on the page shows data.tracking.googleAnalytic as an object field, not a top-level data["tracking.googleAnalytic"] key.
Stop: as in step 1, the page sends data the moment it is saved.
4. Check it fired
Do all three. A tag that is in the page can still fail to send.
- Page source.
view-source:https://<site host>/<page>should containgtag/js?id=G-…,gtag('config','G-…'), andfbq('init','<pixel>')withfbq('track','PageView'). If one is missing, the ID was dropped by the rule in step 1, or it was saved in the wrong place (step 3). - The network. In DevTools › Network, reload. GA sends
https://www.google-analytics.com/g/collect?…tid=G-…and the Pixel sendshttps://www.facebook.com/tr/?id=<pixel>&ev=PageView. Both were seen in a local test. - The vendors' own tools.
- In Google Analytics, use Admin › DebugView. It shows events only from a browser in debug mode: open the site through Tag Assistant (tagassistant.google.com), or use Reports › Realtime.
- For Meta, use the Meta Pixel Helper Chrome extension, or Events Manager › your Pixel › Test events.
One broken line of page code silences all of it. The page's JavaScript, root's and the site's are joined into one <script> with the GA config, the cookies and the Pixel at the end (page-assets.ts 44–60, 121–143). A syntax error anywhere, or an error thrown at the top level, stops everything after it. Tested: a page whose script called document.getElementById('nope').addEventListener(…) still loaded gtag.js, but sent no config, no hit, no Pixel and set no cookies. When tags are in the source but nothing is sent, look for a red error in the console first.
5. Page settings › Style: wait for cookie consent (only if required)
Appmint can't hold back the tags it writes, so a site that needs consent first does it in code. This needs a cookie-consent site notice: its Accept All sets localStorage.site_cookie_consent to 'true' and fires a site-cookie-consent event on window. Reject sets 'minimal' (see Site notice).
- GA: keep the field, use Google Consent Mode.
root's JavaScript runs before the GA tag in that one script (root, then parent, then page, then site, then tracking: page-assets.ts 44–60). So a consent default set in root's Page settings › Style › JavaScript is in place beforeconfig. - Pixel: leave the Pixel field empty and load the Pixel from the same script after consent. Meta's base code quits if
fbqalready exists, so it can't be "pre-blocked" the way GA can.
Tested on a page with a consent notice: before Accept, GA sent only consent-denied pings (gcs=G100), with no _ga cookie and no Pixel. Right after Accept, _ga was set and the Pixel sent PageView. On the next load GA sent normal hits (gcs=G111).
(function () {
if (document.body && document.body.getAttribute('data-design-mode') === 'true') return;
var ok = false;
try { ok = localStorage.getItem('site_cookie_consent') === 'true'; } catch (e) {}
// Google Consent Mode default: set before the site's own gtag('config') runs.
window.dataLayer = window.dataLayer || [];
window.gtag = window.gtag || function () { window.dataLayer.push(arguments); };
var state = ok ? 'granted' : 'denied';
window.gtag('consent', 'default', { ad_storage: state, analytics_storage: state, ad_user_data: state, ad_personalization: state });
// The Pixel, loaded here (Tracking › Pixel left EMPTY) so it can wait.
function loadPixel() {
if (window.fbq) return;
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');
window.fbq('init', '123456789012345');
window.fbq('track', 'PageView');
}
if (ok) loadPixel();
window.addEventListener('site-cookie-consent', function () {
window.gtag('consent', 'update', { ad_storage: 'granted', analytics_storage: 'granted', ad_user_data: 'granted', ad_personalization: 'granted' });
loadPixel();
});
})();Put it in root's Page settings › Style › JavaScript (see Shared header and footer) so it covers every page. Write it with page_write on root's id, and check that it parses first, because it shares one script with everything else. Root's script reaches login, account and checkout only while a My Account layout page or account-layout exists.
- Consent Mode still sends cookieless pings. To send nothing to Google before consent, leave Google Analytics empty as well. Then load
https://www.googletagmanager.com/gtag/js?id=G-…and callgtag('js', new Date()); gtag('config', 'G-…')yourself, in a function that runs only after consent, the wayloadPixeldoes. - Without a cookie-consent notice on the site, nobody can ever accept, and everything stays denied.
Hands on: nothing new.
Check: open the site in a private window. Before you accept, there is no _ga cookie, no facebook.com/tr request, and GA hits carry gcs=G100. After Accept All, _ga appears and the Pixel's PageView is sent.
Stop: root's script runs on every page. Show the person the script and wait for a yes before saving it.
When it's done
Tell the person:
- which IDs are on the site and which page (if any) has its own;
- whether the verification tags are in;
- whether the tags wait for consent or fire for everyone;
- what you saw fire: the source,
g/collect,tr?ev=PageView.
Point them to GA's Realtime report and Meta's Test events to watch it themselves.
What it can't do yet
- No consent gating of its own. The tags fire on load for everyone (page-assets.ts 121–143). Step 5 is the workaround.
- Page views only. GA gets its automatic
page_viewand the Pixel getsPageView. No add-to-cart, purchase, lead or other event is sent by Appmint. A page can callgtag('event', …)orfbq('track', …)from its own JavaScript. - Once per full page load. The tags run in the server-rendered document. A client-side route change doesn't run them again, and doesn't switch to another page's own IDs. A notice link to a page on the same site (
router.push) and some built-in screens change pages this way. The Pixel sends noPageViewfor those. GA counts them only if its own Enhanced measurement › Page changes based on browser history events is on. Plain links and the runtime'snavigate:effect are full loads (base-appruntime/dispatcher.ts447–448). - Not the Tag Manager snippet. A
GTM-…ID is loaded throughgtag/jswith aconfigcall, not Google Tag Manager's owngtm.jssnippet. Untested with a real container. Put a GTM container's standard snippet in root's JavaScript instead. - Page settings can't set a page's tracking (step 3 warning). Use the API.
- One ID of each kind. A second GA property or Pixel has to be added in code:
gtag('config', 'G-SECOND')orfbq('init', '…').
Don'ts
- Don't paste the whole
<script>snippet from Google or Meta into the field. Paste only the ID. - Don't paste a whole
<meta>tag into a verification field. Paste only itscontent. - Don't send a partial
trackingobject withrepo_update. It wipes the cookies and verification codes you didn't resend. - Don't set a page's tracking in Build Studio's Page Config. It never applies.
- Don't put the IDs in a
<script>in the page HTML. Script in page HTML doesn't run there. - Don't tell an EU or UK business it is "consent-ready" because it has a cookie banner. The banner doesn't stop these tags unless you did step 5.
- Don't use real IDs to test on someone else's site, and don't leave test IDs on a live site.